Загрузка данных


Option Explicit

Private Const REPORT_SHEET_DASH As String = "Dash"
Private Const REPORT_SHEET_SUMMARY As String = "Summary"
Private Const REPORT_SHEET_REPAIRS As String = "Repairs"
Private Const REPORT_SHEET_REASONS As String = "Reasons"
Private Const REPORT_SHEET_FILES As String = "Files"

' Type these 3 names manually in VBA editor after pasting:
Private Const SRC_SHEET_DES As String = "TYPE_MANUALLY_1"
Private Const SRC_SHEET_BOILERS As String = "TYPE_MANUALLY_2"
Private Const SRC_SHEET_REPAIRS As String = "TYPE_MANUALLY_3"

' Fixed structure based on your sample files
Private Const DES_COL_RUNNING As Long = 8      ' H
Private Const DES_COL_RESERVE As Long = 9      ' I
Private Const DES_COL_REPAIR As Long = 10      ' J
Private Const DES_COL_LOAD As Long = 11        ' K

' IMPORTANT:
' If DES daily fuel is stored in another column, change only this number.
' Set 0 to disable DES fuel reading.
Private Const DES_FUEL_COL As Long = 0

Private Const BOILERS_FUEL_COL As Long = 19    ' S, daily fuel

Public Sub RefreshMonthData()
    Dim folderPath As String
    Dim fileName As String
    Dim fullPath As String
    Dim wb As Workbook
    Dim wsSummary As Worksheet, wsRepairs As Worksheet, wsReasons As Worksheet
    Dim wsFiles As Worksheet, wsDash As Worksheet
    Dim dictSummary As Object, dictReasons As Object
    Dim repairsRow As Long, filesRow As Long
    Dim reportName As String
    Dim dt As Variant

    On Error GoTo EH

    Application.ScreenUpdating = False
    Application.DisplayAlerts = False
    Application.EnableEvents = False
    Application.AskToUpdateLinks = False
    Application.Calculation = xlCalculationManual
    Application.StatusBar = "Refreshing report..."

    folderPath = ThisWorkbook.Path
    If Len(folderPath) = 0 Then
        MsgBox "Save the report file into the source files folder first.", vbExclamation
        GoTo SafeExit
    End If

    reportName = ThisWorkbook.Name

    Set dictSummary = CreateObject("Scripting.Dictionary")
    Set dictReasons = CreateObject("Scripting.Dictionary")

    PrepareSheets wsDash, wsSummary, wsRepairs, wsReasons, wsFiles

    repairsRow = 2
    filesRow = 2

    fileName = Dir(folderPath & "\*.xls*")
    Do While fileName <> ""
        If LCase(fileName) <> LCase(reportName) Then
            If Left$(fileName, 2) <> "~$" Then
                fullPath = folderPath & "\" & fileName
                dt = ExtractDateFromFilename(fileName)

                If IsDate(dt) Then
                    Set wb = Nothing
                    On Error Resume Next
                    Set wb = Workbooks.Open(Filename:=fullPath, ReadOnly:=True, UpdateLinks:=False, IgnoreReadOnlyRecommended:=True)
                    On Error GoTo EH

                    If Not wb Is Nothing Then
                        ProcessWorkbook wb, CDate(dt), fileName, dictSummary, dictReasons, wsRepairs, repairsRow, wsFiles, filesRow
                        wb.Close SaveChanges:=False
                        Set wb = Nothing
                    Else
                        LogFile wsFiles, filesRow, fileName, CDate(dt), "Error", "Cannot open file"
                        filesRow = filesRow + 1
                    End If
                Else
                    LogFile wsFiles, filesRow, fileName, Empty, "Skipped", "Date not found in filename"
                    filesRow = filesRow + 1
                End If
            End If
        End If
        fileName = Dir
    Loop

    WriteSummary wsSummary, dictSummary
    WriteReasons wsReasons, dictReasons
    BuildDashboard wsDash, wsSummary, wsReasons, wsFiles
    FormatSheets wsSummary, wsRepairs, wsReasons, wsFiles, wsDash

    MsgBox "Update completed.", vbInformation

SafeExit:
    Application.ScreenUpdating = True
    Application.DisplayAlerts = True
    Application.EnableEvents = True
    Application.AskToUpdateLinks = True
    Application.Calculation = xlCalculationAutomatic
    Application.StatusBar = False
    Exit Sub

EH:
    MsgBox "Update error: " & Err.Description, vbCritical
    Resume SafeExit
End Sub

Public Sub CreateRefreshButton()
    Dim ws As Worksheet
    Dim shp As Shape

    Set ws = GetOrCreateSheetSafe(REPORT_SHEET_DASH)

    On Error Resume Next
    ws.Shapes("btnRefresh").Delete
    On Error GoTo 0

    Set shp = ws.Shapes.AddShape(msoShapeRoundedRectangle, 700, 20, 180, 35)
    shp.Name = "btnRefresh"
    shp.TextFrame.Characters.Text = "Refresh data"
    shp.OnAction = "RefreshMonthData"
End Sub

Private Sub PrepareSheets(ByRef wsDash As Worksheet, ByRef wsSummary As Worksheet, ByRef wsRepairs As Worksheet, ByRef wsReasons As Worksheet, ByRef wsFiles As Worksheet)
    Set wsDash = GetOrCreateSheetSafe(REPORT_SHEET_DASH)
    Set wsSummary = GetOrCreateSheetSafe(REPORT_SHEET_SUMMARY)
    Set wsRepairs = GetOrCreateSheetSafe(REPORT_SHEET_REPAIRS)
    Set wsReasons = GetOrCreateSheetSafe(REPORT_SHEET_REASONS)
    Set wsFiles = GetOrCreateSheetSafe(REPORT_SHEET_FILES)

    wsDash.Cells.Clear
    wsSummary.Cells.Clear
    wsRepairs.Cells.Clear
    wsReasons.Cells.Clear
    wsFiles.Cells.Clear

    wsSummary.Range("A1:J1").Value = Array( _
        "Date", "Files", "Repairs", "DES Load kW", "Boilers Fuel", _
        "DES Fuel", "Total Fuel", "DG Running", "DG Reserve", "DG Repair")

    wsRepairs.Range("A1:F1").Value = Array("Date", "Source", "Object/Equipment", "Reason", "Note", "Source File")
    wsReasons.Range("A1:B1").Value = Array("Reason", "Count")
    wsFiles.Range("A1:D1").Value = Array("File", "Date", "Status", "Comment")
End Sub

Private Sub ProcessWorkbook(ByVal wb As Workbook, ByVal dt As Date, ByVal fileName As String, ByVal dictSummary As Object, ByVal dictReasons As Object, ByVal wsRepairs As Worksheet, ByRef repairsRow As Long, ByVal wsFiles As Worksheet, ByRef filesRow As Long)
    Dim desLoad As Double
    Dim boilersFuel As Double
    Dim desFuel As Double
    Dim totalFuel As Double
    Dim cntRepairs As Long
    Dim cntWork As Long, cntReserve As Long, cntRepairState As Long
    Dim okAny As Boolean

    okAny = False

    desLoad = ReadDESLoadAndStatus(wb, cntWork, cntReserve, cntRepairState)
    If desLoad <> 0 Or cntWork <> 0 Or cntReserve <> 0 Or cntRepairState <> 0 Then okAny = True

    boilersFuel = ReadBoilersFuel(wb)
    If boilersFuel <> 0 Then okAny = True

    desFuel = ReadDESFuel(wb)
    If desFuel <> 0 Then okAny = True

    totalFuel = boilersFuel + desFuel

    cntRepairs = ReadRepairs(wb, dt, fileName, dictReasons, wsRepairs, repairsRow)
    If cntRepairs <> 0 Then okAny = True

    AddSummary dictSummary, dt, 1, cntRepairs, desLoad, boilersFuel, desFuel, totalFuel, cntWork, cntReserve, cntRepairState

    If okAny Then
        LogFile wsFiles, filesRow, fileName, dt, "OK", "Processed"
    Else
        LogFile wsFiles, filesRow, fileName, dt, "OK", "Opened, but data not recognized"
    End If
    filesRow = filesRow + 1
End Sub

Private Function ReadDESLoadAndStatus(ByVal wb As Workbook, ByRef cntWork As Long, ByRef cntReserve As Long, ByRef cntRepairState As Long) As Double
    Dim ws As Worksheet
    Dim lastRow As Long, r As Long
    Dim vWork As Variant, vReserve As Variant, vRepair As Variant
    Dim vLoad As Variant
    Dim totalLoad As Double
    Dim idVal As Variant, nameVal As Variant

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_DES)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastUsedRow(ws)
    totalLoad = 0

    For r = 1 To lastRow
        idVal = ws.Cells(r, 1).Value
        nameVal = ws.Cells(r, 2).Value

        If Not IsEmpty(idVal) And Not IsEmpty(nameVal) Then
            If IsNumeric(idVal) Then
                vWork = ws.Cells(r, DES_COL_RUNNING).Value
                vReserve = ws.Cells(r, DES_COL_RESERVE).Value
                vRepair = ws.Cells(r, DES_COL_REPAIR).Value
                vLoad = ws.Cells(r, DES_COL_LOAD).Value

                If Trim$(CStr(vWork)) = "+" Then cntWork = cntWork + 1
                If Trim$(CStr(vReserve)) = "+" Then cntReserve = cntReserve + 1

                If LCase$(Trim$(CStr(vRepair))) = "repair" Or LCase$(Trim$(CStr(vRepair))) = "remont" Or Trim$(CStr(vRepair)) = "+" Then
                    cntRepairState = cntRepairState + 1
                End If

                If IsNumeric(vLoad) Then
                    totalLoad = totalLoad + CDbl(vLoad)
                End If
            End If
        End If
    Next r

    ReadDESLoadAndStatus = totalLoad
End Function

Private Function ReadDESFuel(ByVal wb As Workbook) As Double
    Dim ws As Worksheet
    Dim lastRow As Long, r As Long
    Dim fuelVal As Variant
    Dim totalFuel As Double
    Dim idVal As Variant, nameVal As Variant

    If DES_FUEL_COL <= 0 Then Exit Function

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_DES)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastUsedRow(ws)
    totalFuel = 0

    For r = 1 To lastRow
        idVal = ws.Cells(r, 1).Value
        nameVal = ws.Cells(r, 2).Value

        If Not IsEmpty(idVal) And Not IsEmpty(nameVal) Then
            If IsNumeric(idVal) Then
                fuelVal = ws.Cells(r, DES_FUEL_COL).Value
                If IsNumeric(fuelVal) Then
                    totalFuel = totalFuel + CDbl(fuelVal)
                End If
            End If
        End If
    Next r

    ReadDESFuel = totalFuel
End Function

Private Function ReadBoilersFuel(ByVal wb As Workbook) As Double
    Dim ws As Worksheet
    Dim lastRow As Long, r As Long
    Dim fuelVal As Variant
    Dim totalFuel As Double

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_BOILERS)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastUsedRow(ws)
    totalFuel = 0

    For r = 1 To lastRow
        fuelVal = ws.Cells(r, BOILERS_FUEL_COL).Value
        If IsNumeric(fuelVal) Then
            totalFuel = totalFuel + CDbl(fuelVal)
        End If
    Next r

    ReadBoilersFuel = totalFuel
End Function

Private Function ReadRepairs(ByVal wb As Workbook, ByVal dt As Date, ByVal fileName As String, ByVal dictReasons As Object, ByVal wsRepairs As Worksheet, ByRef repairsRow As Long) As Long
    Dim ws As Worksheet
    Dim lastRow As Long, r As Long
    Dim unitNo As Variant
    Dim typeVal As String, engineVal As String
    Dim reasonVal As String, noteVal As String
    Dim cnt As Long

    On Error Resume Next
    Set ws = wb.Worksheets(SRC_SHEET_REPAIRS)
    On Error GoTo 0
    If ws Is Nothing Then Exit Function

    lastRow = LastUsedRow(ws)
    cnt = 0

    For r = 3 To lastRow
        unitNo = ws.Cells(r, 2).Value
        typeVal = Trim$(CStr(ws.Cells(r, 3).Value))
        engineVal = Trim$(CStr(ws.Cells(r, 4).Value))
        reasonVal = Trim$(CStr(ws.Cells(r, 6).Value))
        noteVal = Trim$(CStr(ws.Cells(r, 8).Value))

        If reasonVal <> "" Then
            If IsNumeric(unitNo) Then
                wsRepairs.Cells(repairsRow, 1).Value = dt
                wsRepairs.Cells(repairsRow, 2).Value = "Equipment repair"
                wsRepairs.Cells(repairsRow, 3).Value = Trim$(typeVal & " " & engineVal)
                wsRepairs.Cells(repairsRow, 4).Value = reasonVal
                wsRepairs.Cells(repairsRow, 5).Value = noteVal
                wsRepairs.Cells(repairsRow, 6).Value = fileName

                repairsRow = repairsRow + 1
                cnt = cnt + 1

                If dictReasons.Exists(reasonVal) Then
                    dictReasons(reasonVal) = CLng(dictReasons(reasonVal)) + 1
                Else
                    dictReasons.Add reasonVal, 1
                End If
            End If
        End If
    Next r

    ReadRepairs = cnt
End Function

Private Sub AddSummary(ByVal dictSummary As Object, ByVal dt As Date, ByVal fileCount As Long, ByVal repairCount As Long, ByVal desLoad As Double, ByVal boilersFuel As Double, ByVal desFuel As Double, ByVal totalFuel As Double, ByVal workCnt As Long, ByVal reserveCnt As Long, ByVal repairStateCnt As Long)
    Dim key As String
    Dim arr As Variant

    key = Format$(dt, "yyyy-mm-dd")

    If dictSummary.Exists(key) Then
        arr = dictSummary(key)
    Else
        ReDim arr(1 To 9)
        arr(1) = 0
        arr(2) = 0
        arr(3) = 0#
        arr(4) = 0#
        arr(5) = 0#
        arr(6) = 0#
        arr(7) = 0
        arr(8) = 0
        arr(9) = 0
    End If

    arr(1) = arr(1) + fileCount
    arr(2) = arr(2) + repairCount
    arr(3) = arr(3) + desLoad
    arr(4) = arr(4) + boilersFuel
    arr(5) = arr(5) + desFuel
    arr(6) = arr(6) + totalFuel
    arr(7) = arr(7) + workCnt
    arr(8) = arr(8) + reserveCnt
    arr(9) = arr(9) + repairStateCnt

    dictSummary(key) = arr
End Sub

Private Sub WriteSummary(ByVal ws As Worksheet, ByVal dictSummary As Object)
    Dim keys As Variant, i As Long, r As Long, arr As Variant

    If dictSummary.Count = 0 Then Exit Sub

    keys = dictSummary.Keys
    SortStringDates keys

    r = 2
    For i = LBound(keys) To UBound(keys)
        arr = dictSummary(keys(i))
        ws.Cells(r, 1).Value = CDate(keys(i))
        ws.Cells(r, 2).Value = arr(1)
        ws.Cells(r, 3).Value = arr(2)
        ws.Cells(r, 4).Value = arr(3)
        ws.Cells(r, 5).Value = arr(4)
        ws.Cells(r, 6).Value = arr(5)
        ws.Cells(r, 7).Value = arr(6)
        ws.Cells(r, 8).Value = arr(7)
        ws.Cells(r, 9).Value = arr(8)
        ws.Cells(r, 10).Value = arr(9)
        r = r + 1
    Next i
End Sub

Private Sub WriteReasons(ByVal ws As Worksheet, ByVal dictReasons As Object)
    Dim keys As Variant, i As Long, r As Long
    Dim lastRow As Long

    If dictReasons.Count = 0 Then Exit Sub

    keys = dictReasons.Keys
    r = 2

    For i = LBound(keys) To UBound(keys)
        ws.Cells(r, 1).Value = keys(i)
        ws.Cells(r, 2).Value = dictReasons(keys(i))
        r = r + 1
    Next i

    lastRow = LastUsedRow(ws)
    If lastRow >= 2 Then
        ws.Range("A1:B" & lastRow).Sort Key1:=ws.Range("B2"), Order1:=xlDescending, Header:=xlYes
    End If
End Sub

Private Sub BuildDashboard(ByVal wsDash As Worksheet, ByVal wsSummary As Worksheet, ByVal wsReasons As Worksheet, ByVal wsFiles As Worksheet)
    Dim lastSummaryRow As Long, lastReasonsRow As Long
    Dim ch As ChartObject

    wsDash.Cells.Clear

    wsDash.Range("A1").Value = "Monthly Dashboard"
    wsDash.Range("A1").Font.Size = 18
    wsDash.Range("A1").Font.Bold = True

    wsDash.Range("A3").Value = "Files processed"
    wsDash.Range("B3").Formula = "=MAX(0,COUNTA(Files!A:A)-1)"

    wsDash.Range("D3").Value = "Days with data"
    wsDash.Range("E3").Formula = "=MAX(0,COUNTA(Summary!A:A)-1)"

    wsDash.Range("A4").Value = "Total repairs"
    wsDash.Range("B4").Formula = "=SUM(Summary!C:C)"

    wsDash.Range("D4").Value = "Total DES load"
    wsDash.Range("E4").Formula = "=SUM(Summary!D:D)"

    wsDash.Range("A5").Value = "Boilers fuel"
    wsDash.Range("B5").Formula = "=SUM(Summary!E:E)"

    wsDash.Range("D5").Value = "DES fuel"
    wsDash.Range("E5").Formula = "=SUM(Summary!F:F)"

    wsDash.Range("A6").Value = "Total fuel"
    wsDash.Range("B6").Formula = "=SUM(Summary!G:G)"

    wsDash.Range("D6").Value = "Last date"
    wsDash.Range("E6").Formula = "=MAX(Summary!A:A)"
    wsDash.Range("E6").NumberFormat = "dd.mm.yyyy"

    lastSummaryRow = LastUsedRow(wsSummary)
    lastReasonsRow = LastUsedRow(wsReasons)

    DeleteAllCharts wsDash

    If lastSummaryRow >= 2 Then
        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=120, Width:=420, Height:=220)
        With ch.Chart
            .ChartType = xlColumnClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Repairs"""
            .SeriesCollection(1).XValues = wsSummary.Range("A2:A" & lastSummaryRow)
            .SeriesCollection(1).Values = wsSummary.Range("C2:C" & lastSummaryRow)
            .HasTitle = True
            .ChartTitle.Text = "Repairs by day"
        End With

        Set ch = wsDash.ChartObjects.Add(Left:=460, Top:=120, Width:=420, Height:=220)
        With ch.Chart
            .ChartType = xlLine
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""DES load"""
            .SeriesCollection(1).XValues = wsSummary.Range("A2:A" & lastSummaryRow)
            .SeriesCollection(1).Values = wsSummary.Range("D2:D" & lastSummaryRow)
            .HasTitle = True
            .ChartTitle.Text = "DES load by day"
        End With

        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=360, Width:=420, Height:=220)
        With ch.Chart
            .ChartType = xlLine
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Total fuel"""
            .SeriesCollection(1).XValues = wsSummary.Range("A2:A" & lastSummaryRow)
            .SeriesCollection(1).Values = wsSummary.Range("G2:G" & lastSummaryRow)
            .HasTitle = True
            .ChartTitle.Text = "Total fuel by day"
        End With

        Set ch = wsDash.ChartObjects.Add(Left:=460, Top:=360, Width:=420, Height:=220)
        With ch.Chart
            .ChartType = xlColumnClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""DG status"""
            .SeriesCollection(1).XValues = Array("Running", "Reserve", "Repair")
            .SeriesCollection(1).Values = Array(wsSummary.Cells(lastSummaryRow, 8).Value, wsSummary.Cells(lastSummaryRow, 9).Value, wsSummary.Cells(lastSummaryRow, 10).Value)
            .HasTitle = True
            .ChartTitle.Text = "DG status"
        End With
    End If

    If lastReasonsRow >= 2 Then
        Set ch = wsDash.ChartObjects.Add(Left:=20, Top:=600, Width:=860, Height:=250)
        With ch.Chart
            .ChartType = xlBarClustered
            ClearSeries ch.Chart
            .SeriesCollection.NewSeries
            .SeriesCollection(1).Name = "=""Reasons"""
            .SeriesCollection(1).XValues = wsReasons.Range("A2:A" & WorksheetFunction.Min(lastReasonsRow, 11))
            .SeriesCollection(1).Values = wsReasons.Range("B2:B" & WorksheetFunction.Min(lastReasonsRow, 11))
            .HasTitle = True
            .ChartTitle.Text = "Top repair reasons"
        End With
    End If
End Sub

Private Sub ClearSeries(ByVal ch As Chart)
    On Error Resume Next
    Do While ch.SeriesCollection.Count > 0
        ch.SeriesCollection(1).Delete
    Loop
    On Error GoTo 0
End Sub

Private Sub DeleteAllCharts(ByVal ws As Worksheet)
    Dim i As Long
    For i = ws.ChartObjects.Count To 1 Step -1
        ws.ChartObjects(i).Delete
    Next i
End Sub

Private Sub FormatSheets(ByVal wsSummary As Worksheet, ByVal wsRepairs As Worksheet, ByVal wsReasons As Worksheet, ByVal wsFiles As Worksheet, ByVal wsDash As Worksheet)
    FormatOneSheet wsSummary
    FormatOneSheet wsRepairs
    FormatOneSheet wsReasons
    FormatOneSheet wsFiles

    wsSummary.Columns("A").NumberFormat = "dd.mm.yyyy"
    wsRepairs.Columns("A").NumberFormat = "dd.mm.yyyy"
    wsFiles.Columns("B").NumberFormat = "dd.mm.yyyy"

    wsDash.Columns("A:F").ColumnWidth = 16
    wsDash.Range("A3:E6").Font.Bold = True
End Sub

Private Sub FormatOneSheet(ByVal ws As Worksheet)
    With ws.Rows(1)
        .Font.Bold = True
        .Interior.Color = RGB(217, 225, 242)
    End With

    ws.Cells.EntireColumn.AutoFit

    On Error Resume Next
    ws.Rows(1).AutoFilter
    On Error GoTo 0
End Sub

Private Sub LogFile(ByVal ws As Worksheet, ByVal rowNum As Long, ByVal fileName As String, ByVal dt As Variant, ByVal statusText As String, ByVal commentText As String)
    ws.Cells(rowNum, 1).Value = fileName
    If IsDate(dt) Then ws.Cells(rowNum, 2).Value = CDate(dt)
    ws.Cells(rowNum, 3).Value = statusText
    ws.Cells(rowNum, 4).Value = commentText
End Sub

Private Function GetOrCreateSheetSafe(ByVal sheetName As String) As Worksheet
    Dim ws As Worksheet
    Dim candidate As String
    Dim i As Long

    For Each ws In ThisWorkbook.Worksheets
        If LCase$(ws.Name) = LCase$(sheetName) Then
            Set GetOrCreateSheetSafe = ws
            Exit Function
        End If
    Next ws

    Set ws = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))

    candidate = CleanSheetName(sheetName)
    If candidate = "" Then candidate = "SheetX"

    On Error Resume Next
    ws.Name = candidate
    If Err.Number = 0 Then
        On Error GoTo 0
        Set GetOrCreateSheetSafe = ws
        Exit Function
    End If
    Err.Clear

    For i = 1 To 99
        ws.Name = Left$(candidate, 28) & "_" & CStr(i)
        If Err.Number = 0 Then Exit For
        Err.Clear
    Next i
    On Error GoTo 0

    Set GetOrCreateSheetSafe = ws
End Function

Private Function CleanSheetName(ByVal s As String) As String
    Dim badChars As Variant
    Dim ch As Variant

    badChars = Array(":", "\", "/", "?", "*", "[", "]")
    CleanSheetName = Trim$(s)

    For Each ch In badChars
        CleanSheetName = Replace(CleanSheetName, CStr(ch), "")
    Next ch

    If Len(CleanSheetName) > 31 Then
        CleanSheetName = Left$(CleanSheetName, 31)
    End If
End Function

Private Function ExtractDateFromFilename(ByVal fileName As String) As Variant
    Dim re As Object, m As Object
    Dim d As Integer, mn As Integer, y As Integer

    Set re = CreateObject("VBScript.RegExp")
    re.Global = False
    re.IgnoreCase = True
    re.Pattern = "(\d{1,2})[.\-_](\d{1,2})[.\-_](\d{4})"

    If re.Test(fileName) Then
        Set m = re.Execute(fileName)(0)
        d = CInt(m.SubMatches(0))
        mn = CInt(m.SubMatches(1))
        y = CInt(m.SubMatches(2))
        On Error Resume Next
        ExtractDateFromFilename = DateSerial(y, mn, d)
        If Err.Number <> 0 Then ExtractDateFromFilename = Empty
        On Error GoTo 0
    Else
        ExtractDateFromFilename = Empty
    End If
End Function

Private Function LastUsedRow(ByVal ws As Worksheet) As Long
    Dim f As Range

    On Error Resume Next
    Set f = ws.Cells.Find(What:="*", After:=ws.Range("A1"), LookAt:=xlPart, LookIn:=xlFormulas, SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
    On Error GoTo 0

    If f Is Nothing Then
        LastUsedRow = 1
    Else
        LastUsedRow = f.Row
    End If
End Function

Private Function LastUsedColInRow(ByVal ws As Worksheet, ByVal rowNum As Long) As Long
    LastUsedColInRow = ws.Cells(rowNum, ws.Columns.Count).End(xlToLeft).Column
    If LastUsedColInRow < 1 Then LastUsedColInRow = 1
End Function

Private Sub SortStringDates(ByRef arr As Variant)
    Dim i As Long, j As Long
    Dim tmp As Variant

    For i = LBound(arr) To UBound(arr) - 1
        For j = i + 1 To UBound(arr)
            If CDate(arr(j)) < CDate(arr(i)) Then
                tmp = arr(i)
                arr(i) = arr(j)
                arr(j) = tmp
            End If
        Next j
    Next i
End Sub