Attribute VB_Name = "Common_File_Utils" Option Explicit ' ============================================================ ' Module Name: Write_Common ' Module Desc: CSV write functions ' Module Methods: ' - GetSaveCSVPath ' - WriteCSVFromArray ' ============================================================ Function GetSaveCSVPath(Optional ByVal defaultName As String = "") As String Dim savePath As String savePath = Application.GetSaveAsFilename( _ FileFilter:="CSV Files (*.csv), *.csv", _ Title:="Save CSV", _ InitialFileName:=defaultName) If savePath = "False" Or savePath = "" Then GetSaveCSVPath = "" Exit Function End If If InStr(1, savePath, ".csv", vbTextCompare) = 0 Then savePath = savePath & ".csv" End If GetSaveCSVPath = savePath End Function ' Writes a 2D array to a CSV file Sub WriteCSVFromArray( _ ByVal filePath As String, _ ByVal data As Variant, _ Optional ByVal Charset As String = "shift_jis", _ Optional ByVal alwaysQuote As Boolean = False _ ) ' === Input validation === If Not IsArray(data) Then Err.Raise ERR_FILE_INVALID_ARRAY, "WriteCSVFromArray", "Input 'data' must be an array." End If ' === Check if 2D array === Dim numDims As Long: numDims = ArrayDimensions(data) If numDims <> 2 Then Err.Raise ERR_FILE_NOT_2D, "WriteCSVFromArray", "Input array must be 2-dimensional." End If Dim rows As Long, cols As Long rows = UBound(data, 1) - LBound(data, 1) + 1 cols = UBound(data, 2) - LBound(data, 2) + 1 If rows = 0 Or cols = 0 Then Exit Sub ' Empty array, exit early ' === Build CSV content === Dim outputLines As VBA.Collection Set outputLines = New VBA.Collection Dim i As Long, j As Long Dim rowStr As String Dim field As String Dim needsQuote As Boolean For i = LBound(data, 1) To UBound(data, 1) Dim fields As Variant ReDim fields(1 To cols) For j = LBound(data, 2) To UBound(data, 2) ' Safely convert variant to string field = SafeToString(data(i, j)) ' Determine if the field needs quoting (per RFC 4180) needsQuote = alwaysQuote Or (InStr(field, """") > 0) Or _ (InStr(field, ",") > 0) Or _ (InStr(field, vbLf) > 0) Or _ (InStr(field, vbCrLf) > 0) Or _ (InStr(field, vbCr) > 0) Or _ (Left(field, 1) = " " Or Right(field, 1) = " ") If needsQuote Then ' Escape double quotes: "" represents a single " field = """" & Replace(field, """", """""") & """" End If field = GetCode(field) fields(j - LBound(data, 2) + 1) = field Next j rowStr = Join(fields, ",") outputLines.Add rowStr Next i ' Concatenate all lines Dim finalContent As String finalContent = Join(CollectionToArray(outputLines), vbCrLf) ' === Write to file === Dim stream As Object Set stream = CreateObject("ADODB.Stream") With stream .Type = 2 ' adTypeText .Charset = Charset .Open .WriteText finalContent, 0 ' adWriteChar .SaveToFile filePath, 2 ' adSaveCreateOverWrite .Close End With End Sub ' Helper function: safely convert any Variant to a string Private Function SafeToString(ByVal v As Variant) As String If IsNull(v) Or IsEmpty(v) Then SafeToString = "" Exit Function End If SafeToString = CStr(v) End Function ' Helper function: get the number of dimensions of an array (1, 2, ...) Private Function ArrayDimensions(arr As Variant) As Long Dim dimCount As Long On Error GoTo ExitPoint Do dimCount = dimCount + 1 Dim tmp As Long tmp = UBound(arr, dimCount) Loop ExitPoint: ArrayDimensions = dimCount - 1 End Function ' Helper function: convert a VBA.Collection to a 1D array (for use with Join) Private Function CollectionToArray(col As VBA.Collection) As Variant If col.Count = 0 Then CollectionToArray = Array() Exit Function End If Dim arr() As String ReDim arr(1 To col.Count) Dim i As Long For i = 1 To col.Count arr(i) = col(i) Next i CollectionToArray = arr End Function ' ============================================================ ' Module Name: Read_Common ' Module Desc: CSV read functions ' Module Methods: ' - SelectCSVFile ' - ReadCSVAs2DArrayStrict ' - ParseCSVLines ' ============================================================ Function SelectCSVFile() As String Dim fileDialog As FileDialog Set fileDialog = Application.FileDialog(msoFileDialogFilePicker) With fileDialog .Filters.Clear .Filters.Add "CSV Files", "*.csv" .AllowMultiSelect = False If .Show <> -1 Then SelectCSVFile = "" Exit Function End If SelectCSVFile = .SelectedItems(1) End With End Function ' Read a CSV file and return its content as a strict 2D array (1-based). ' All rows must have the same number of columns as the first row. ' If the file is empty and defaultColumnCount > 0, returns an empty 2D array with zero rows and defaultColumnCount columns. ' Parameters: ' filePath: Full path to the CSV file. ' charset: Text encoding (e.g., "cp932", "utf-8"). ' defaultColumnCount: Optional. Used only if CSV has no rows. Must be >= 0. Function ReadCSVAs2DArrayStrict( _ ByVal filePath As String, _ ByVal expectedColumnCount As Long, _ Optional ByVal charset As String = "cp932", _ Optional ByVal hasHeader As Boolean = False) As Variant ' === validate expectedColumnCount === If expectedColumnCount <= 0 Then Err.Raise ERR_FILE_INVALID_PARAM, "ReadCSVAs2DArrayStrict", "expectedColumnCount must be >= 1." End If If Dir(filePath) = "" Then Err.Raise ERR_FILE_NOT_FOUND, "ReadCSVAs2DArrayStrict", "File not found: " & filePath End If ' === read csv file === Dim stream As Object Set stream = CreateObject("ADODB.Stream") With stream .Type = 2 ' adTypeText .charset = charset .Open .LoadFromFile filePath Dim textContent As String textContent = .ReadText .Close End With ' === standardize === textContent = Replace(textContent, vbCrLf, vbLf) textContent = Replace(textContent, vbCr, vbLf) ' === transfer into collection === Dim lines As VBA.Collection Set lines = ParseCSVLines(textContent) ' === validate empty === If lines.Count = 0 Then Err.Raise ERR_FILE_EMPTY, "ReadCSVAs2DArrayStrict", "CSV file is empty." End If If lines.Count = 1 Then If hasHeader Then Err.Raise ERR_FILE_NO_DATA, "ReadCSVAs2DArrayStrict", "CSV file data is empty." End If End If ' === loop the row, validate column count === Dim i As Long For i = 1 To lines.Count Dim rowArr As Variant rowArr = lines(i) Dim actualCols As Long actualCols = UBound(rowArr) - LBound(rowArr) + 1 If actualCols <> expectedColumnCount Then Err.Raise ERR_FILE_COLUMN_MISMATCH, "ReadCSVAs2DArrayStrict", "Row " & i & ": Expected " & expectedColumnCount & " columns, got " & actualCols & "." End If Next i Dim result As Variant Dim startRow As Long If hasHeader Then startRow = 2 Else startRow = 1 End If ReDim result(startRow To lines.Count, 1 To expectedColumnCount) For i = startRow To lines.Count rowArr = lines(i) Dim j As Long For j = LBound(rowArr) To UBound(rowArr) result(i, j - LBound(rowArr) + 1) = rowArr(j) Next j Next i ReadCSVAs2DArrayStrict = result End Function ' Helper function: Parse CSV text into collection of string arrays (zero-based per row) Private Function ParseCSVLines(ByVal csvText As String) As VBA.Collection Set ParseCSVLines = New VBA.Collection Dim length As Long: length = Len(csvText) If length = 0 Then Exit Function Dim i As Long: i = 1 Dim currentField As String Dim currentRow As VBA.Collection: Set currentRow = New VBA.Collection Dim inQuotes As Boolean Dim c As String Do While i <= length c = Mid$(csvText, i, 1) Select Case c Case """" If inQuotes Then If i < length And Mid$(csvText, i + 1, 1) = """" Then currentField = currentField & """" i = i + 2 Else inQuotes = False i = i + 1 End If Else inQuotes = True i = i + 1 End If Case "," If inQuotes Then currentField = currentField & c i = i + 1 Else ' Clean field before adding currentField = Trim(currentField) currentField = Replace(currentField, vbCr, "") currentField = Replace(currentField, vbLf, "") currentRow.Add currentField currentField = "" i = i + 1 End If Case vbLf If inQuotes Then currentField = currentField & c i = i + 1 Else currentRow.Add currentField Dim arr() As String If currentRow.Count > 0 Then ReDim arr(0 To currentRow.Count - 1) Dim k As Long For k = 1 To currentRow.Count arr(k - 1) = currentRow(k) Next k End If ParseCSVLines.Add arr Set currentRow = New VBA.Collection currentField = "" inQuotes = False i = i + 1 End If Case Else currentField = currentField & c i = i + 1 End Select Loop ' Handle last row without trailing newline If currentField <> "" Or currentRow.Count > 0 Then ' Clean field before adding currentField = Trim(currentField) currentField = Replace(currentField, vbCr, "") currentField = Replace(currentField, vbLf, "") currentRow.Add currentField Dim lastArr() As String If currentRow.Count > 0 Then ReDim lastArr(0 To currentRow.Count - 1) Dim m As Long For m = 1 To currentRow.Count lastArr(m - 1) = currentRow(m) Next m End If ParseCSVLines.Add lastArr End If End Function