Robby
Robby

Reputation: 827

Export Excel data to fixed-width text file - field locations

Let me begin by saying I'm kind of new to working with delimited files. I am trying to emulate how a piece of software lays out a text file using Excel.

Here is the code I'm using to create a text file from the worksheet:

Sub Export_Selection_As_Fixed_Length_File()
     ' Dimension all  variables.
    Dim DestinationFile, CellValue, Filler_Char_To_Replace_Blanks As String
    Dim FileNum, ColumnCount, RowCount, FieldWidth As Integer
    Dim sht As Worksheet

    'Below are options incase you want to change the folder where VBA stores the .txt file
    'We use ActiveWorkbook.Path in this example
    'ActiveWorkbook.Path 'the activeworkbook
    'ThisWorkbook.Path  'the workbook with the code
    'CurDir  'the current directory (when you hit File|open)


    'If a cell is blank, what character should be used instead
    Filler_Char_To_Replace_Blanks = " "

        'Check if the user has made any selection at all
        If Selection.Cells.Count < 2 Then
            MsgBox "Nothing selected to export"
            Selection.Activate
            End
        End If

    'This is the destination file name.
    DestinationFile = ActiveWorkbook.Path & "/textfile.txt"
    'Obtain next free file handle number.
    FileNum = FreeFile()

     ' Turn  error checking off.
    On Error Resume Next

     ' Attempt to open destination file for output.
    Open DestinationFile For Output As #FileNum

     ' If an error occurs report it and end.
    If Err <> 0 Then
         MsgBox "Cannot open filename " & DestinationFile
         Selection.Activate
        End
    End If

     ' Turn error checking on.
    On Error GoTo 0

     '  Loop for each row in selection.
    For RowCount = 1 To Selection.Rows.Count
                For ColumnCount = 1 To Selection.Columns.Count
                    CellValue = Selection.Cells(RowCount, ColumnCount).Text
                    If (IsNull(CellValue) Or CellValue = "") Then CellValue = Filler_Char_To_Replace_Blanks
                    FieldWidth = Cells(1, ColumnCount).Value
                    If (ColumnCount = Selection.Columns.Count) Then
                            Print #FileNum, Format$(CellValue, "!" & String(FieldWidth, "@")) & vbCrLf;
                    Else: Print #FileNum, Format$(CellValue, "!" & String(FieldWidth, "@"));
                    End If
                Next ColumnCount
         ' Start next iteration of RowCount loop.
    Next RowCount
     ' Close destination file.
    Close #FileNum
    Selection.Activate
    Workbooks.OpenText Filename:=DestinationFile
End Sub

The software I'm trying to emulate has "data locations" and "field sizes." For example, one field has a data location of 77, which means it will start as the 77th character on the line in the text file. (I don't know how common this is, so if it's very common, please excuse the useless information.) And the field size is 12.

If that doesn't make sense, here's a screenshot of a text file. The first line shows what my VBA creates, and the second line is how I want it to look. How can I force the values on the worksheet to start at a certain position on the line based on the column it's in?

enter image description here

Upvotes: 0

Views: 3031

Answers (1)

Jamie Riis
Jamie Riis

Reputation: 411

It looks like your first row in the selection contains the field's width FieldWidth = Cells(1, ColumnCount).Value. In your problem description you mentioned data locations and field sizes. You need to have this information some where. You could put it on another sheet in the file, which would let you adjust the output of text file, or you could put those values in your VBA code as constants, or your could create a Class. Using something like this will enable you can redefine the fields as needed. The example below uses a simple class and a few private functions in the module

In the example below you'll need to add a sheet named "FieldControl" and place the appropriate values in columns..See the GetFieldControl function. To test the code I used the following:

enter image description here

You'll need to add the following reference to your macro work book. In the VBA editor under the Tools menu select References, then when the dialog box appears select Microsoft Scripting Runtime. (Tools->References)

And with all things code related, there are improvements that could be made to this.

Good Luck with your efforts

The Class (Insert->Class) change the default name to clField (you can call it whatever you like but make sure to update the dim statement GetFieldControl function to match the name you gave it.)

Option Explicit

Public Enum eFieldType
    Number
    Text
End Enum

Public Name As String
Public Size As Long
Public StartPos As Long
Public Value As String
Public FieldType As eFieldType

The module with a few updates

Option Explicit
Option Base 1    'This makes any defined array start a 1 rather than 0

Sub Export_Selection_As_Fixed_Length_File()
     ' Dimension all  variables.
    Dim DestinationFile, CellValue, Filler_Char_To_Replace_Blanks As String
    Dim FileNum, ColumnCount, RowCount, FieldWidth As Integer
    Dim sht As Worksheet

    Dim outputRecord() As String
    'Below are options in case you want to change the folder where VBA stores the .txt file
    'We use ActiveWorkbook.Path in this example
    'ActiveWorkbook.Path 'the activeworkbook
    'ThisWorkbook.Path  'the workbook with the code
    'CurDir  'the current directory (when you hit File|open)

    'If a cell is blank, what character should be used instead
    Filler_Char_To_Replace_Blanks = "+"

    'Check if the user has made any selection at all
    If Selection.Cells.Count < 2 Then
        MsgBox "Nothing selected to export"
        Selection.Activate
        End
    End If

    'This is the destination file name.
    DestinationFile = ActiveWorkbook.Path & "\textfile.txt"  'This was changed to the DOS version of directory separator

    On Error GoTo catchFileOpenError    'Poor man's version of Try/Catch

    'Get a FileSystemObject using the MSFT Scripting Runtime reference
    Dim fd As Scripting.FileSystemObject
    Set fd = New Scripting.FileSystemObject

    Dim outputFile As Object
    Set outputFile = fd.CreateTextFile(DestinationFile, True, False)

    ' Turn error checking on.
    On Error GoTo 0

    Dim record As Scripting.Dictionary
    'Call a private function that gets the filed control information from the
    'Sheet titled FieldControl and the associated range
    Set record = GetFieldControl(ActiveWorkbook.Sheets("FieldControl").Range("A2:D7"))

    'Declare enumerators to loop through the selection
    Dim dataRow As Range
    Dim dataFld As Range

    'Declare the output buffer, 80 characters
    Dim outputBuffer(80) As Byte
    'loop thru the selection row by row
    For Each dataRow In Selection.Rows
        'Initialize buffer to empty value defined by the second parameter
        Call InitOutputBuffer(outputBuffer, Filler_Char_To_Replace_Blanks)
        'Loop thru each field in the row
        For Each dataFld In dataRow.Columns
            'Copy the input value into the output byte array
            Call CopyStringToByteArray(outputBuffer, StrConv(Trim(CStr(dataFld.Value2)), vbFromUnicode), _
                        record(dataFld.Column).StartPos, record(dataFld.Column).FieldType, record(dataFld.Column).Size)
        Next dataFld
        'Write the record to the text file but first convert ASCII Byte to Unicode String
        'Also this method places CR/LF as part of the output to the file
        outputFile.WriteLine StrConv(outputBuffer, vbUnicode)
    Next dataRow

     ' Close destination file.
    outputFile.Close

    Selection.Activate
    Workbooks.OpenText Filename:=DestinationFile
    Exit Sub

catchFileOpenError:     'Catch the error after trying if openning the file fails
    On Error GoTo 0
    MsgBox "Cannot open filename " & DestinationFile
    Selection.Activate
End Sub
'***********************************************************************************
'*
'* PARAMETERS:
'*  outBuf is the updated buffer
'*  inBuf is the input buffer that needs to be copied to the output buffer (buffer)
'*  startCol is the starting column for the field
'*  fldTy is the field type as defined by the class enumerator eFieldType
'*  fldLen is the length of the field as defined on the control sheet
Private Sub CopyStringToByteArray(ByRef outBuf() As Byte, ByRef inBuf() As Byte, _
                ByVal startCol As Long, ByRef fldTy As eFieldType, ByVal fldLen As Long)
    Dim idx As Long
    If fldTy = Text Then       'Left Justified
        For idx = LBound(inBuf) To UBound(inBuf)
            outBuf(startCol) = inBuf(idx)
            startCol = startCol + 1
        Next idx
    Else                        'Right Justified
        Dim revIdx As Long
        revIdx = startCol + fldLen - 1
        For idx = UBound(inBuf) To LBound(inBuf) Step -1
            outBuf(revIdx) = inBuf(idx)
            revIdx = revIdx - 1
        Next idx
    End If
End Sub
'***************************************************************************
'*  InitOutputBuffer
'*      PARAMETERS:
'*          buffer is the buffer to initialize
'*          initVal is a string containing the value used to initialize the buffer
Private Sub InitOutputBuffer(ByRef buffer() As Byte, ByVal initVal As String)
    Dim byInitVal() As Byte 'Byte array to hold the values from the string conversion
    byInitVal = StrConv(initVal, vbFromUnicode) 'convert the string into an ASCII array
    Dim idx As Long
    For idx = LBound(buffer) To UBound(buffer)
        buffer(idx) = byInitVal(0)
    Next idx

    'buffer(81) = Asc(Chr(13)) 'Carriage Return Character
    'buffer(82) = Asc(Chr(10)) 'Line Feed Character

End Sub

'*******************************************************************************
'*
'*  GetFieldControl
'*      PARAMETERS:
'*          ctrlRng is the range on a worksheet where the field control info is
'*              found
'*      REMARKS:
'*          The range needs to have the following columns: Name, Size, Start Postion
'*          and Type.  Type values can be Text or Number
Private Function GetFieldControl(ByRef ctrlRng As Range) As Scripting.Dictionary
    Dim retVal As Scripting.Dictionary
    Set retVal = New Scripting.Dictionary

    'format of control range is : Name, Size, Start Position, Type
    Dim fldInfoRow As Range
    Dim fld As clField  'A class that holds the control values from the work sheet
    Dim colCnt As Long: colCnt = 1  'Becomes the key for the dictionary
    For Each fldInfoRow In ctrlRng.Rows
        Set fld = New clField
        fld.Name = fldInfoRow.Value2(1, 1)      'Name of field in data table
        fld.Size = fldInfoRow.Value2(1, 2)      'Output Size of field
        fld.StartPos = fldInfoRow.Value2(1, 3)  'Output starting position for this field
        Select Case fldInfoRow.Value2(1, 4)     'Controls how the output value is formated
            Case "Text"                         '  Text left justified, Numbers are right justified
                fld.FieldType = Text
            Case "Number"
                fld.FieldType = Number
            Case Default
                fld.FieldType = Text
        End Select
        retVal.Add Key:=colCnt, Item:=fld   'Add the key and the fld object to the dictionary
        colCnt = colCnt + 1                 'This key value is mapped to the column number in the input data table
    Next fldInfoRow

    'Return the scripting Dictionary
    Set GetFieldControl = retVal
End Function

Upvotes: 1

Related Questions