How to create a simple program to update a csv file

It is very common for applications to use CSV files as configuration files or for mocking data for testing purposes. Some of these files can be quite large, and the challenge arises when we need to update many rows at once.

Opening CSV files in Excel can sometimes introduce unintended characters or alter the original file structure. As a result, Git may show numerous changes that were not actually intended, caused only by formatting modifications introduced by Excel.

The program below was written in Java and allows you to pass, as parameters, the CSV file you want to process, the criteria for identifying rows based on column values, and the updates that need to be applied.

package util;

import java.io.*;
import java.nio.file.*;
import java.util.*;

/**
 * Usage:
 *   java CsvModifier <csvFilePath> "<col='val',...>" "<col='val',...>"
 *
 * Example (plain replacement):
 *   java CsvModifier c:\temp\config.csv "configName='Load_Data1',country='Brazil'" "label=Changed,quote='100'"
 *
 * Example (substring replacement):
 *   java CsvModifier c:\temp\config.csv "configName='Load_Data1',country='Brazil'" "label=Changed,quote='100',complement='replaceSubstring('value1','value2')'"
 *
 * Criteria and update values may optionally be wrapped in single quotes.
 * Use replaceSubstring('old','new') as an update value to replace a substring
 * within the existing cell content rather than overwriting the whole cell.
 */
public class CsvModifier {

    public static void main(String[] args) throws IOException {
        if (args.length != 3) {
            System.err.println("Usage: CsvModifier <csvFile> \"<criteria>\" \"<updates>\"");
            System.err.println("  criteria: col='val',col2='val2'  (quotes optional)");
            System.err.println("  updates : col='val',col2='val2'  (quotes optional)");
            System.exit(1);
        }

        Path csvPath = Paths.get(args[0]);
        Map<String, String> criteria = parseKeyValuePairs(args[1]);
        Map<String, String> updates  = parseKeyValuePairs(args[2]);

        List<String> lines = Files.readAllLines(csvPath);
        if (lines.isEmpty()) {
            System.out.println("CSV file is empty – nothing to do.");
            return;
        }

        // Parse header
        String headerLine = lines.get(0);
        String[] headers  = splitCsvLine(headerLine);

        // Build index: column name -> position
        Map<String, Integer> colIndex = new LinkedHashMap<>();
        for (int i = 0; i < headers.length; i++) {
            colIndex.put(headers[i].trim(), i);
        }

        // Validate criteria columns
        for (String col : criteria.keySet()) {
            if (!colIndex.containsKey(col)) {
                System.err.println("Criteria column not found in header: " + col);
                System.exit(1);
            }
        }

        // Validate update columns
        for (String col : updates.keySet()) {
            if (!colIndex.containsKey(col)) {
                System.err.println("Update column not found in header: " + col);
                System.exit(1);
            }
        }

        int updatedRows = 0;
        List<String> resultLines = new ArrayList<>();
        resultLines.add(headerLine);   // keep original header

        for (int lineIdx = 1; lineIdx < lines.size(); lineIdx++) {
            String line = lines.get(lineIdx);
            if (line.isBlank()) {
                resultLines.add(line);
                continue;
            }

            String[] fields = splitCsvLine(line);
            // Ensure array is large enough (in case last columns are empty)
            if (fields.length < headers.length) {
                fields = Arrays.copyOf(fields, headers.length);
            }

            if (matchesCriteria(fields, colIndex, criteria)) {
                for (Map.Entry<String, String> update : updates.entrySet()) {
                    int idx = colIndex.get(update.getKey());
                    fields[idx] = applyUpdateValue(fields[idx], update.getValue());
                }
                updatedRows++;
            }

            resultLines.add(joinCsvLine(fields));
        }

        Files.write(csvPath, resultLines);
        System.out.println("Done. Rows updated: " + updatedRows);
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    /**
     * Applies an update directive to the current field value.
     *
     * If the directive matches  replaceSubstring('oldVal','newVal')  the method
     * replaces every occurrence of oldVal with newVal inside the existing field.
     * Otherwise the directive is treated as a plain replacement value.
     */
    static String applyUpdateValue(String currentValue, String directive) {
        String trimmed = directive.trim();
        if (trimmed.toLowerCase().startsWith("replacesubstring(")) {
            String[] args = parseReplaceSubstringArgs(trimmed);
            String oldSub = args[0];
            String newSub = args[1];
            return currentValue == null ? null : currentValue.replace(oldSub, newSub);
        }
        // Plain replacement – strip surrounding single quotes if present
        if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length() >= 2) {
            return trimmed.substring(1, trimmed.length() - 1);
        }
        return trimmed;
    }

    /**
     * Extracts the two single-quoted arguments from a replaceSubstring('a','b') directive.
     * Returns a two-element array: [oldSubstring, newSubstring].
     */
    static String[] parseReplaceSubstringArgs(String directive) {
        // Find content between the outer parentheses
        int open  = directive.indexOf('(');
        int close = directive.lastIndexOf(')');
        if (open < 0 || close < 0 || close <= open) {
            throw new IllegalArgumentException("Invalid replaceSubstring syntax: " + directive);
        }
        String inner = directive.substring(open + 1, close).trim(); // 'value1','value2'

        // Split on the comma that separates the two quoted arguments
        // Pattern: '<anything>','<anything>'
        String[] parts = inner.split("',\\s*'");
        if (parts.length != 2) {
            throw new IllegalArgumentException("replaceSubstring requires exactly 2 arguments: " + directive);
        }
        // Strip the remaining leading/trailing single quote from each part
        String first  = parts[0].startsWith("'") ? parts[0].substring(1) : parts[0];
        String second = parts[1].endsWith("'")   ? parts[1].substring(0, parts[1].length() - 1) : parts[1];
        return new String[]{first, second};
    }

    /**
     * Parses a string like: batchName='Load_Data1',country='Brazil'
     * Single quotes around values are optional and are stripped.
     */
    static Map<String, String> parseKeyValuePairs(String input) {
        Map<String, String> map = new LinkedHashMap<>();
        // Split on commas that are NOT inside single quotes
        String[] pairs = input.split(",(?=(?:[^']*'[^']*')*[^']*$)");
        for (String pair : pairs) {
            pair = pair.trim();
            int eq = pair.indexOf('=');
            if (eq < 0) {
                throw new IllegalArgumentException("Invalid key=value pair: " + pair);
            }
            String key   = pair.substring(0, eq).trim();
            String value = pair.substring(eq + 1).trim();
            // Strip surrounding single quotes from value
            if (value.startsWith("'") && value.endsWith("'") && value.length() >= 2) {
                value = value.substring(1, value.length() - 1);
            }
            map.put(key, value);
        }
        return map;
    }

    /**
     * Simple CSV line splitter (handles commas inside double-quoted fields).
     */
    static String[] splitCsvLine(String line) {
        List<String> tokens = new ArrayList<>();
        StringBuilder sb    = new StringBuilder();
        boolean inQuotes    = false;
        for (int i = 0; i < line.length(); i++) {
            char c = line.charAt(i);
            if (c == '"') {
                inQuotes = !inQuotes;
            } else if (c == ',' && !inQuotes) {
                tokens.add(sb.toString());
                sb.setLength(0);
            } else {
                sb.append(c);
            }
        }
        tokens.add(sb.toString());
        return tokens.toArray(new String[0]);
    }

    /**
     * Re-joins fields with commas. Wraps fields containing commas in double quotes.
     */
    static String joinCsvLine(String[] fields) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < fields.length; i++) {
            if (i > 0) sb.append(',');
            String f = fields[i] == null ? "" : fields[i];
            if (f.contains(",") || f.contains("\"")) {
                sb.append('"').append(f.replace("\"", "\"\"")).append('"');
            } else {
                sb.append(f);
            }
        }
        return sb.toString();
    }

    private static boolean matchesCriteria(String[] fields,
                                           Map<String, Integer> colIndex,
                                           Map<String, String> criteria) {
        for (Map.Entry<String, String> entry : criteria.entrySet()) {
            int idx = colIndex.get(entry.getKey());
            String actual = idx < fields.length ? fields[idx] : "";
            if (actual == null) actual = "";
            if (!actual.trim().equals(entry.getValue())) {
                return false;
            }
        }
        return true;
    }
}

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top