3

I'm used to write the content (values) of a range of cells to a text file with the write command in VBA, for example:

write #myfile, Range("A1").value, Range("A2).value, Range("A3).value

Does there exist a more elegant and convenient built-in method to dump a whole range directly to a delimited file, possibly even over multiple rows at a time? Or has anybody come up with a customized solution? I think that would be incredibly useful.

Steve06
  • 741
  • 2
  • 15
  • 32
  • See my solution below. Using variant arrays rather than range loops makes a huge difference to code run-time – brettdj Oct 26 '12 at 00:29

4 Answers4

5

I wrote you this it could still be improved, but I think it's good enough:

Sub SaveRangeAsCSV(r As Range, filename As String, overwrite As Boolean)
    Dim wB As Workbook
    Dim c As Range
    Dim usedRows As Long
    If overwrite Then
        If Dir(filename) <> "" Then Kill filename
        If Err.Number <> 0 Then
            MsgBox "Could not delete previously existing file." & vbNewLine & Err.Number & ": " & Err.Description
            Exit Sub
        End If
    End If
    If Dir(filename) <> "" Then
        Set wB = Workbooks.Open(filename)
    Else
        Set wB = Workbooks.Add
    End If

    With wB.Sheets(1)
        usedRows = .UsedRange.Rows.Count
        'Check if more than 1 row is in the used range.
        If usedRows = 1 Then
            'Since there's only 1 row, see if there's more than 1 cell.
            If .UsedRange.Cells.Count = 1 Then
                'Since there's only 1 cell, check the contents
                If .Cells(1, 1) = "" Then
                      'you're dealing with a blank workbook
                      usedRows = 0
                End If
            End If
        End If
        'Check if range is contigious
        If InStr(r.Address, ",") Then
            For Each c In r.Cells
                .Range(c.Address).Offset(usedRows, 0).Value = c.Value
            Next
        Else
            .Range(r.Address).Offset(usedRows, 0).Value = r.Value
        End If
    End With
    wB.SaveAs filename, xlCSV, , , , False
    wB.Saved = True
    wB.Close
End Sub
Sub Example()
    'I used Selection here just to make it easier to test.
    'Substitute your actual range, and actual desired filepath
    'If you pass false for overwrite, it assumes you want to append
    'It will give you a pop-up asking if you want to overwrite, which I could avoid
    'by copying the worksheet and then closing and deleting the file etc... but I
    'already spent enough time on this one.
    SaveRangeAsCSV Selection, "C:\proofOfConcept.csv", False
End Sub

When using it, just supply the actual range, the actual filename, and whether or not you want to overwrite the file. :) This has been updated to allow non-contiguous ranges. For merged cells it will end up putting the value in the first cell of the merged range.

Daniel
  • 12,982
  • 3
  • 36
  • 60
  • Very kind of you. Looks good. I wonder if it works with non-contiguous ranges? And I would need to be able to append new ranges to the file as I go. Does the wB.SaveAs method allow this? I thought it works only for saving "one shot" of data. – Steve06 Oct 25 '12 at 21:24
  • You're right, this will fail for non-contiguous ranges. I'm posting an update to correct that issue. :) – Daniel Oct 25 '12 at 21:34
  • Nice, but does this "SaveAs-Method" also allow to append new ranges to the bottom of an existing file? My guess is it overwrites? – Steve06 Oct 25 '12 at 21:40
  • It absolutely would over-write, I just updated this again so that you can tell Excel not to bother you about the fact that you are overwriting (otherwise it will ask you if the file already exists). If you want to append to the file, I guess we could do that by opening the CSV, figuring out where to append the data and then resaving. But... it would probably more reasonable to just append using standard file I/O since the file already exists. – Daniel Oct 25 '12 at 21:51
  • I edited this again to allow you to append to the file, but I didn't write around the pop-up because I've already spent a bit of time on this and my daughter keeps wanting me to stop ignoring her. :) – Daniel Oct 25 '12 at 22:12
  • Thanks, I guess it is very efficent for large amounts of data due to the use of the SaveAs worksheet function. – Steve06 Oct 25 '12 at 22:13
2

This is the solution I came up with by myself and suits my needs best as far as I can see:

Sub DumpRangeToTextFile(filehandle As Integer, source As Range)
Dim row_range As Range, mycell As Range
For Each row_range In source.rows
    For Each mycell In row_range.cells
        Write #filehandle, mycell.Value;
    Next mycell
    Write #filehandle,
Next row_range
End Sub

Short and sweet! ;)

Still I'm giving Daniel Cook's solution which is also very useful the credit it deserves.

Steve06
  • 741
  • 2
  • 15
  • 32
1

These methods above iterate across the cell ranges in order to export the data. Anything that tends to do with looping over a range of cells in the sheet is extremely slow due to all the error checking.

Here's a way I did it without the iteration. Basically, it makes use of the built in function "Join()" to do the heavy lifting which would be your iteration loop. This is much faster.

The related Read() subroutine I detailed in another posting: https://stackoverflow.com/a/35688988/2800701

This is the Write() subroutine (note: this assumes your text is pre-formatted to the correct specification in the worksheet before you export it; it will only work on a single column...not on multiple column ranges):

Public Sub WriteRangeAsPlainText(ExportRange As Range, Optional textfilename As Variant)
   If IsMissing(textfilename) Then textfilename = Application.GetSaveAsFilename(FileFilter:="Text Files (*.txt), *.txt")
   If textfilename = "" Then Exit Sub

   Dim filenumber As Integer
   filenumber = FreeFile
   Open textfilename For Output As filenumber

   Dim textlines() As Variant, outputvar As Variant

   textlines = Application.Transpose(ExportRange.Value)
   outputvar = Join(textlines, vbCrLf)
   Print #filenumber, outputvar
   Close filenumber
End Sub
Community
  • 1
  • 1
RexBarker
  • 1,456
  • 16
  • 14
0

From my article Creating and Writing to a CSV FIle using Excel VBA

This Article provides two VBA code samples to create and write to a CSV file:

  1. Creating a CSV file using the Open For Output as FreeFile.
  2. Creating a CSV file using the FileSystemObject object.

I prefer the latter approach mainly as I am using the FileSystemObject for further coding, for example processing all files in subfolders recursively (though that technique is not used in this article).

Code Notes

This code must be run from a regular VBA Code Module. Otherwise the code will cause an error if users try to run it from the ThisWorkbook or Sheet Code panes given the usage of Const.

It is worth noting that the ThisWorkbook and Sheet code sections should be reserved for Event coding only, "normal" VBA should be run from standard Code Modules.

Please note that for purposes of the sample code, the file path of the CSV output file is "hard-coded" as: C:\test\myfile.csv at the top of the code. You will probably want to set the output file programmatically, for instance as a function parameter.

As mentioned earlier; For example purposes, this code TRANSPOSES COLUMNS AND ROWS; that is, the output file contains one CSV row for each column in the selected range. Normally, CSV output would be row-by-row, echoing the layout visible on screen, but I wanted to demonstrate that generating the output by using VBA code provides options beyond what is available by, for instance, using the Save As... CSV Text menu option.

Code

Const sFilePath = "C:\test\myfile.csv"
Const strDelim = ","

'Option 1
Sub CreateCSV_Output()
Dim ws As Worksheet
Dim rng1 As Range
Dim X
Dim lRow As Long
Dim lCol As Long
Dim strTmp As String
Dim lFnum As Long

lFnum = FreeFile
Open sFilePath For Output As lFnum

For Each ws In ActiveWorkbook.Worksheets
    'test that sheet has been used
    Set rng1 = ws.UsedRange
    If Not rng1 Is Nothing Then
        'only multi-cell ranges can be written to a 2D array
        If rng1.Cells.Count > 1 Then
            X = ws.UsedRange.Value2
            'The code TRANSPOSES COLUMNS AND ROWS by writing strings column by column
            For lCol = 1 To UBound(X, 2)
                'write initial value outside the loop
                 strTmp = IIf(InStr(X(1, lCol), strDelim) > 0, """" & X(1, lCol) & """", X(1, lCol))
                For lRow = 2 To UBound(X, 1)
                    'concatenate long string & (short string with short string)
                    strTmp = strTmp & (strDelim & IIf(InStr(X(lRow, lCol), strDelim) > 0, """" & X(lRow, lCol) & """", X(lRow, lCol)))
                Next lRow
                'write each line to CSV
                Print #lFnum, strTmp
            Next lCol
        Else
            Print #lFnum, IIf(InStr(ws.UsedRange.Value, strDelim) > 0, """" & ws.UsedRange.Value & """", ws.UsedRange.Value)
        End If
    End If
Next ws

Close lFnum
MsgBox "Done!", vbOKOnly

End Sub

'Option 2
Sub CreateCSV_FSO()
Dim objFSO
Dim objTF
Dim ws As Worksheet
Dim lRow As Long
Dim lCol As Long
Dim strTmp As String
Dim lFnum As Long

Set objFSO = CreateObject("scripting.filesystemobject")
Set objTF = objFSO.createtextfile(sFilePath, True, False)

For Each ws In ActiveWorkbook.Worksheets
    'test that sheet has been used
    Set rng1 = ws.UsedRange
    If Not rng1 Is Nothing Then
        'only multi-cell ranges can be written to a 2D array
        If rng1.Cells.Count > 1 Then
            X = ws.UsedRange.Value2
            'The code TRANSPOSES COLUMNS AND ROWS by writing strings column by column
            For lCol = 1 To UBound(X, 2)
                'write initial value outside the loop
                strTmp = IIf(InStr(X(1, lCol), strDelim) > 0, """" & X(1, lCol) & """", X(1, lCol))
                For lRow = 2 To UBound(X, 1)
                    'concatenate long string & (short string with short string)
                    strTmp = strTmp & (strDelim & IIf(InStr(X(lRow, lCol), strDelim) > 0, """" & X(lRow, lCol) & """", X(lRow, lCol)))
                Next lRow
                'write each line to CSV
                objTF.writeline strTmp
            Next lCol
        Else
            objTF.writeline IIf(InStr(ws.UsedRange.Value, strDelim) > 0, """" & ws.UsedRange.Value & """", ws.UsedRange.Value)
        End If
    End If
Next ws

objTF.Close
Set objFSO = Nothing
MsgBox "Done!", vbOKOnly

End Sub
brettdj
  • 54,857
  • 16
  • 114
  • 177