Data & Text Processing

Three recipes for working with structured data - filtering CSV datasets, converting JSON API responses to CSV, and generating formatted reports from multiple sources.

Back to Kookbook


CSV Filter

Modules: CSV · List · Dictionary · File · String · Konsol

Reads products.csv, keeps only rows matching a given category, and writes the filtered rows to filtered_<category>.csv. Demonstrates the core CSV read/filter/write loop that underpins most data-pipeline scripts.

Key patterns:

Ships with products.csv (sample product catalogue).

Usage

minks csv_filter.ks <category>

minks csv_filter.ks electronics
minks csv_filter.ks clothing

Sample output

Loaded 8 rows.  Filtering by category: electronics
Matched 3 row(s) → filtered_electronics.csv
Done.

Sample data

name,category,price,stock
Widget Pro,electronics,29.99,150
USB Hub,electronics,12.50,320
Headphones,electronics,79.99,60
Monitor Stand,electronics,45.00,95
Office Chair,furniture,199.00,45
Desk Lamp,furniture,34.99,80
Notebook,stationery,4.99,500
Pen Set,stationery,8.99,250

Script

// csv_filter.ks - Filter rows in a CSV file by category and write matches to a new file
// Usage: minks csv_filter.ks <input.csv> <category>
//
// Try with the bundled sample:
//   minks csv_filter.ks products.csv electronics
//   minks csv_filter.ks products.csv furniture
//
// The script reads a CSV with headers: name, category, price, stock
// Rows where the category column matches the given value are written to
// filtered_<category>.csv in the current directory.
//
// Modules: CSV, File, String, List, OS

// ── 1. Parse arguments ────────────────────────────────────────────────────────

List:New args:String;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 2) {
    Konsol:Print("Usage: minks csv_filter.ks <input.csv> <category>");
    Konsol:Exit(1);
}

Var:String inputFile;
Var:String filterCategory;
List:Get(0, args, inputFile);
List:Get(1, args, filterCategory);

// ── 2. Read and parse the CSV ─────────────────────────────────────────────────
// File:ReadAll reads the entire file into a string in one call.
// CSV:Parse loads that string into a named in-memory document.
// The document name (here: inp) is a bare identifier, not a quoted string.

Var:Boolean fileExists;
File:Exists(inputFile, fileExists);
if (fileExists == false) {
    Konsol:Print("File not found: ${inputFile}");
    Konsol:Exit(1);
}

Var:Number fh;
Var:String content;
File:Open(inputFile, "r", fh);
File:ReadAll(fh, content);
File:Close(fh);

CSV:Parse(content, inp);

Var:Number totalRows;
CSV:Rows(inp, totalRows);

Var:Number dataRows = totalRows - 1;
Konsol:Print("Scanning ${dataRows} rows for category '${filterCategory}'...");

// ── 3. Copy the header into the output document ───────────────────────────────
// Cells are always strings; rows and columns are 0-based.

Var:String hName; Var:String hCategory; Var:String hPrice; Var:String hStock;
CSV:Get(0, 0, inp, hName);
CSV:Get(0, 1, inp, hCategory);
CSV:Get(0, 2, inp, hPrice);
CSV:Get(0, 3, inp, hStock);

// CSV:Set extends the output document (out) on demand - no need to pre-size it.
CSV:Set(0, 0, hName,     out);
CSV:Set(0, 1, hCategory, out);
CSV:Set(0, 2, hPrice,    out);
CSV:Set(0, 3, hStock,    out);

// ── 4. Filter rows where column 1 (category) matches the argument ─────────────

Var:Number i      = 1;   // start at 1 to skip the header row
Var:Number outRow = 1;   // next available row in the output document
Var:Number matched = 0;
Var:String rowName; Var:String rowCategory; Var:String rowPrice; Var:String rowStock;

while (i < totalRows) {
    CSV:Get(i, 0, inp, rowName);
    CSV:Get(i, 1, inp, rowCategory);
    CSV:Get(i, 2, inp, rowPrice);
    CSV:Get(i, 3, inp, rowStock);

    // == compares string values directly.
    if (rowCategory == filterCategory) {
        CSV:Set(outRow, 0, rowName,     out);
        CSV:Set(outRow, 1, rowCategory, out);
        CSV:Set(outRow, 2, rowPrice,    out);
        CSV:Set(outRow, 3, rowStock,    out);
        outRow  = outRow  + 1;
        matched = matched + 1;
        Konsol:Print("  match: ${rowName}");
    }

    i = i + 1;
}

Konsol:Print("${matched} row(s) matched.");

// ── 5. Write the output CSV ───────────────────────────────────────────────────
// Only write if there is at least one match.

if (matched > 0) {
    // Build the output filename: "filtered_" + category + ".csv"
    Var:String stem;
    Var:String outPath;
    String:Join("_", "filtered", filterCategory, stem);
    String:Join("", stem, ".csv", outPath);

    Var:String csv;
    CSV:Stringify(out, csv);

    Var:Number wh;
    File:Open(outPath, "w", wh);
    File:Write(csv, wh);
    File:Close(wh);

    Konsol:Print("Written to: ${outPath}");
}

CSV:Free(inp);
CSV:Free(out);


JSON to CSV Converter

Modules: JSON · Dictionary · File · Konsol

Reads users.json (a JSON array under the "users" key), extracts each user's fields by zero-based index path, and writes a flat users.csv.

Key patterns:

Ships with users.json (sample user list).

Usage

minks json_to_csv.ks

Sample output

Converting users.json → users.csv
Wrote 5 user(s).
Done.

Sample data

{
  "users": [
    {"id": 1, "name": "Alice Chen",    "email": "alice@example.com",  "role": "admin"},
    {"id": 2, "name": "Bob Torres",    "email": "bob@example.com",    "role": "user"},
    {"id": 3, "name": "Carol Kim",     "email": "carol@example.com",  "role": "user"},
    {"id": 4, "name": "Dave Patel",    "email": "dave@example.com",   "role": "moderator"},
    {"id": 5, "name": "Eve Johnson",   "email": "eve@example.com",    "role": "user"},
    {"id": 6, "name": "Frank Müller",  "email": "frank@example.com",  "role": "moderator"},
    {"id": 7, "name": "Grace Okafor", "email": "grace@example.com",  "role": "user"}
  ]
}

Script

// json_to_csv.ks - Read a JSON array of objects and write selected fields to CSV
// Usage: minks json_to_csv.ks <input.json> <output.csv>
//
// Try with the bundled sample:
//   minks json_to_csv.ks users.json users.csv
//
// Expects a JSON structure with a top-level "users" array where each element
// has: id, name, email, role.
// Extend the field list at the bottom to reshape for your own JSON shape.
//
// Modules: JSON, CSV, File, String, List, OS

// ── 1. Parse arguments ────────────────────────────────────────────────────────

List:New args:String;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 2) {
    Konsol:Print("Usage: minks json_to_csv.ks <input.json> <output.csv>");
    Konsol:Exit(1);
}

Var:String jsonFile;
Var:String csvFile;
List:Get(0, args, jsonFile);
List:Get(1, args, csvFile);

// ── 2. Read and parse the JSON file ──────────────────────────────────────────
// JSON:Parse loads the JSON string into a named in-memory document.
// The document name (here: doc) is a bare identifier, not a quoted string.

Var:Boolean fileExists;
File:Exists(jsonFile, fileExists);
if (fileExists == false) {
    Konsol:Print("File not found: ${jsonFile}");
    Konsol:Exit(1);
}

Var:Number fh;
Var:String content;
File:Open(jsonFile, "r", fh);
File:ReadAll(fh, content);
File:Close(fh);

JSON:Parse(content, doc);

// Get the number of elements in the "users" array.
Var:Number userCount;
JSON:Length("users", doc, userCount);
Konsol:Print("Found ${userCount} users in '${jsonFile}'.");

// ── 3. Write the CSV header ───────────────────────────────────────────────────
// CSV documents (here: out) are named bare identifiers.

CSV:Set(0, 0, "id",    out);
CSV:Set(0, 1, "name",  out);
CSV:Set(0, 2, "email", out);
CSV:Set(0, 3, "role",  out);

// ── 4. Iterate the JSON array and populate the CSV ───────────────────────────
// JSON:Get uses dot-notation paths. Numeric segments index arrays:
//   "users.0.name" → first user's name
//   "users.1.email" → second user's email
// String interpolation (${i}) builds these paths dynamically.

Var:Number i = 0;
Var:Number csvRow = 1;   // row 0 is the header
Var:String userId; Var:String userName; Var:String userEmail; Var:String userRole;

while (i < userCount) {
    JSON:Get("users.${i}.id",    doc, userId);
    JSON:Get("users.${i}.name",  doc, userName);
    JSON:Get("users.${i}.email", doc, userEmail);
    JSON:Get("users.${i}.role",  doc, userRole);

    CSV:Set(csvRow, 0, userId,    out);
    CSV:Set(csvRow, 1, userName,  out);
    CSV:Set(csvRow, 2, userEmail, out);
    CSV:Set(csvRow, 3, userRole,  out);

    csvRow = csvRow + 1;
    i = i + 1;
}

// ── 5. Serialize and write the CSV file ──────────────────────────────────────

Var:String csvContent;
CSV:Stringify(out, csvContent);

Var:Number wh;
File:Open(csvFile, "w", wh);
File:Write(csvContent, wh);
File:Close(wh);

Konsol:Print("Written ${userCount} row(s) to '${csvFile}'.");

JSON:Free(doc);
CSV:Free(out);


Report Generator

Modules: CSV · JSON · String · File · Dictionary · Konsol

Combines both sources - products.csv and users.json - into a single formatted text report: per-category product totals and a breakdown of users by role.

Key patterns:

Usage

minks report_generator.ks

Sample output

Generating report...
Products: 8 rows across 3 categories.
Users: 5 records across 2 roles.
Report written to report.txt
Done.

Script

// report_generator.ks - Combine a CSV and a JSON source into a formatted text report
// Usage: minks report_generator.ks <products.csv> <users.json> <report.txt>
//
// Try with the bundled samples:
//   minks report_generator.ks products.csv users.json report.txt
//
// Reads:
//   products.csv - name, category, price, stock
//   users.json   - { "users": [ { id, name, email, role } ... ] }
//
// Writes a formatted report.txt with:
//   - Product count grouped by category  (CSV → Dictionary)
//   - User count grouped by role         (JSON → Dictionary)
//
// Modules: CSV, JSON, Dictionary, List, File, String, OS

// ── 1. Parse arguments ────────────────────────────────────────────────────────

List:New args:String;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 3) {
    Konsol:Print("Usage: minks report_generator.ks <products.csv> <users.json> <report.txt>");
    Konsol:Exit(1);
}

Var:String csvFile;
Var:String jsonFile;
Var:String reportFile;
List:Get(0, args, csvFile);
List:Get(1, args, jsonFile);
List:Get(2, args, reportFile);

// ── 2. Load the CSV ───────────────────────────────────────────────────────────

Var:Boolean fileExists;
File:Exists(csvFile, fileExists);
if (fileExists == false) {
    Konsol:Print("CSV not found: ${csvFile}");
    Konsol:Exit(1);
}

Var:Number fh;
Var:String content;
File:Open(csvFile, "r", fh);
File:ReadAll(fh, content);
File:Close(fh);

CSV:Parse(content, products);

Var:Number productRows;
CSV:Rows(products, productRows);

// ── 3. Group products by category using a Dictionary ──────────────────────────
// Dictionary:New creates an empty string-keyed dictionary.
// Dictionary:Has checks whether a key already exists (so we can initialize or increment).

Dictionary:New catCounts;

Var:Number i = 1;   // row 0 is the header
Var:String cat;
Var:Boolean hasKey;
Var:Number catCount;

while (i < productRows) {
    CSV:Get(i, 1, products, cat);   // column 1 = category

    Dictionary:Has(cat, catCounts, hasKey);
    if (hasKey) {
        Dictionary:Get(cat, catCounts, catCount);
        catCount = catCount + 1;
    } else {
        catCount = 1;
    }
    Dictionary:Set(cat, catCount, catCounts);

    i = i + 1;
}

// ── 4. Load the JSON ──────────────────────────────────────────────────────────

File:Exists(jsonFile, fileExists);
if (fileExists == false) {
    Konsol:Print("JSON not found: ${jsonFile}");
    Konsol:Exit(1);
}

Var:Number fh2;
Var:String jsonContent;
File:Open(jsonFile, "r", fh2);
File:ReadAll(fh2, jsonContent);
File:Close(fh2);

JSON:Parse(jsonContent, users);

Var:Number userCount;
JSON:Length("users", users, userCount);

// ── 5. Group users by role using a second Dictionary ──────────────────────────

Dictionary:New roleCounts;

Var:Number j = 0;
Var:String role;
Var:Boolean hasRole;
Var:Number roleCount;

while (j < userCount) {
    JSON:Get("users.${j}.role", users, role);

    Dictionary:Has(role, roleCounts, hasRole);
    if (hasRole) {
        Dictionary:Get(role, roleCounts, roleCount);
        roleCount = roleCount + 1;
    } else {
        roleCount = 1;
    }
    Dictionary:Set(role, roleCount, roleCounts);

    j = j + 1;
}

// ── 6. Write the report ───────────────────────────────────────────────────────
// File:Write appends the string argument to the open file handle.
// \n is the newline escape inside string literals.

Var:Number wh;
File:Open(reportFile, "w", wh);

File:Write("==============================\n", wh);
File:Write("  Combined Data Report\n", wh);
File:Write("==============================\n\n", wh);

// Products section - iterate Dictionary:Keys to print each category count.
File:Write("Products by Category\n", wh);
File:Write("--------------------\n", wh);

List:New catKeys:String;
Dictionary:Keys(catCounts, catKeys);
Var:Number catKeyCount;
List:Size(catKeys, catKeyCount);
Var:Number k = 0;
Var:String catKey;
Var:Number catVal;

while (k < catKeyCount) {
    List:Get(k, catKeys, catKey);
    Dictionary:Get(catKey, catCounts, catVal);
    File:Write("  ${catKey}: ${catVal} item(s)\n", wh);
    k = k + 1;
}

Var:Number totalProducts = productRows - 1;
File:Write("  ──\n", wh);
File:Write("  Total: ${totalProducts} product(s)\n\n", wh);

// Users section
File:Write("Users by Role\n", wh);
File:Write("-------------\n", wh);

List:New roleKeys:String;
Dictionary:Keys(roleCounts, roleKeys);
Var:Number roleKeyCount;
List:Size(roleKeys, roleKeyCount);
Var:Number m = 0;
Var:String roleKey;
Var:Number roleVal;

while (m < roleKeyCount) {
    List:Get(m, roleKeys, roleKey);
    Dictionary:Get(roleKey, roleCounts, roleVal);
    File:Write("  ${roleKey}: ${roleVal} user(s)\n", wh);
    m = m + 1;
}

File:Write("  ──\n", wh);
File:Write("  Total: ${userCount} user(s)\n", wh);

File:Close(wh);

Konsol:Print("Report written to '${reportFile}'.");

CSV:Free(products);
JSON:Free(users);


Back to Kookbook