Skip to main content
deleted 17 characters in body
Source Link
Jamal
  • 34.9k
  • 13
  • 133
  • 237

I used this isthis as a guide while writing it: http://www.csvreader.com/csv_format.php

Thankyou for any feedback.

I used this is a guide while writing it: http://www.csvreader.com/csv_format.php

Thankyou for any feedback.

I used this as a guide while writing it.

edited tags
Link
h.j.k.
  • 19.2k
  • 3
  • 36
  • 93
Source Link

Parsing a single CSV line into a list of strings

I've written this method to replace an older method that was much simpler, but used the regex split method and couldn't tell if a comma was in quotes/brackets/etc. and didn't read double quotes as single. I was hoping to cover most of the most used possible CSV formats except where a tab is used as a separator rather than a comma.

I haven't tested this yet, and I'm going to write a unit test later on, but I was hoping to get some feedback on it anyway. What I might be missing, what I might be able to do better or more concisely, etc.

It does seem a little long for my liking.

I used this is a guide while writing it: http://www.csvreader.com/csv_format.php

Thankyou for any feedback.

private enum PositionState
{ inQuotes, inInvertedCommas, inBrackets, inSquareBrackets, inCurlyBrackets, inChevronBrackets }

private List<String> splitCSVLine(String toSplit)
{
    List<String> entries = new ArrayList<String>();
    boolean nextCharIsEscaped = false;
    boolean lastCharIsEscapingQuote = false;
    StringBuilder entryBuilder = new StringBuilder();
    List<PositionState> positionStates = new ArrayList<PositionState>();
    
    for(int i = 0; i < toSplit.length(); i++)
    {
        char iChar = toSplit.charAt(i);
        boolean printCharacter = true; // Whether or not the character should be printed into the current entry.
        
        switch(iChar)
        {
            case '\\':
            {
                if(!nextCharIsEscaped)
                    printCharacter = false;
                
                nextCharIsEscaped = !nextCharIsEscaped;
            } break;
                
            case ',':
            {
                if(nextCharIsEscaped)
                    nextCharIsEscaped = false;
                else if(positionStates.isEmpty())
                {
                    entries.add(entryBuilder.toString());
                    entryBuilder = new StringBuilder();
                    printCharacter = false;
                }
            } break;
                
            case '\'':
            {
                if(nextCharIsEscaped)
                    nextCharIsEscaped = false;
                else
                {
                    boolean isOpening = true;

                    for(int j = positionStates.size() - 1; j >= 0; j--)
                        if(positionStates.get(j) == PositionState.inInvertedCommas)
                        {
                            isOpening = false;
                            
                            for(int k = positionStates.size() - 1; k >= j; k--)
                                positionStates.remove(k);
                            
                            break;
                        }
                    
                    if(isOpening)
                        positionStates.add(PositionState.inInvertedCommas);
                }
            } break;
                
            case '"':
            {
                if(nextCharIsEscaped)
                    nextCharIsEscaped = false;
                else if(lastCharIsEscapingQuote)
                    lastCharIsEscapingQuote = false;
                else if(i < toSplit.length() - 1 && toSplit.charAt(i + 1) == '"')
                    lastCharIsEscapingQuote = true;
                else
                {
                    boolean isOpening = true;

                    for(int j = positionStates.size() - 1; j >= 0; j--)
                        if(positionStates.get(j) == PositionState.inQuotes)
                        {
                            isOpening = false;

                            for(int k = positionStates.size() - 1; k >= j; k--)
                                positionStates.remove(k);

                            break;
                        }

                    if(isOpening)
                        positionStates.add(PositionState.inQuotes);
                }
            } break;
                
            case '(':
            {
                if(nextCharIsEscaped)
                    nextCharIsEscaped = false;
                else
                    positionStates.add(PositionState.inBrackets);
            } break;
                
            case ')':
            {
                for(int j = positionStates.size() - 1; j >= 0; j--)
                    if(positionStates.get(j) == PositionState.inBrackets)
                    {
                        for(int k = positionStates.size() - 1; k >= j; k--)
                            positionStates.remove(k);
                        
                        break;
                    }
            } break;
                
            case '[':
            {
                if(nextCharIsEscaped)
                    nextCharIsEscaped = false;
                else
                    positionStates.add(PositionState.inSquareBrackets);
            } break;
                
            case ']':
            {
                for(int j = positionStates.size() - 1; j >= 0; j--)
                    if(positionStates.get(j) == PositionState.inSquareBrackets)
                    {
                        for(int k = positionStates.size() - 1; k >= j; k--)
                            positionStates.remove(k);
                        
                        break;
                    }
            } break;
                
            case '{':
            {
                if(nextCharIsEscaped)
                    nextCharIsEscaped = false;
                else
                    positionStates.add(PositionState.inCurlyBrackets);
            } break;
                
            case '}':
            {
                for(int j = positionStates.size() - 1; j >= 0; j--)
                    if(positionStates.get(j) == PositionState.inCurlyBrackets)
                    {
                        for(int k = positionStates.size() - 1; k >= j; k--)
                            positionStates.remove(k);
                        
                        break;
                    }
            } break;
        }
        
        if(printCharacter)
            entryBuilder.append(iChar);
    }
    
    entries.add(entryBuilder.toString());
    handleQuotes(entries);
    return entries;
}

/**
 * Removes the surrounding quotes from entries that are enclosed in them, and converts adjacent pairs of quotation
 * marks into single ones, where they don't represent an empty field and the first quotation mark isn't escaped.
 * @param entries The list of entries to handle quotes in.
 */
private void handleQuotes(List<String> entries)
{
    for(int i = 0; i < entries.size(); i++)
    {
        String iEntry = entries.get(i);
        String iEntryOriginal = iEntry;
        
        iEntry = iEntry.trim();
        
        if(iEntry.startsWith("\"") && iEntry.endsWith("\""))
            iEntry = iEntry.substring(1, iEntry.length() - 1);
        
        /*
            Go through iEntry looking for double quotes (""). Double-quotes representing empty fields have already
            been taken out with the previous check. Replace them with a single quote (") where the first quote isn't
            escaped. That is, where they aren't preceded by an escape character (\) that isn't itself escaped.
            This can be approximated by checking whether they're preceded by an odd or even number of the escape
            character.
        */
        
        for(int j = 0; j < iEntry.length() - 1; j++)
        {
            if(iEntry.charAt(j) == '"' && iEntry.charAt(j + 1) == '"')
            {
                int escapeCharacterCount = 0;
                
                for(int k = j - 1; k >= 0; k--)
                {
                    if(iEntry.charAt(k) == '\\')
                        escapeCharacterCount++;
                    else
                        break;
                }
                
                if(escapeCharacterCount % 2 == 0) // if escapeCharacterCount is even or 0
                    iEntry = new StringBuilder(iEntry).deleteCharAt(j + 1).toString();
            }
        }
        
        if(!iEntry.equals(iEntryOriginal))
            entries.set(i, iEntry);
    }
}