Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion CsvSharp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@
PrintTable(headers, records.Take(limit));
break;

case "query":
var sql = args.ElementAtOrDefault(2);
if (sql == null) { Console.Error.WriteLine("Usage: csvsharp query <file.csv> '<SQL>'"); return 1; }
QueryTable(headers, records, sql);
break;

case "stats":
ShowStats(headers, records);
break;

default:
Console.Error.WriteLine($"Unknown command: {command}");
return 1;
Expand Down Expand Up @@ -95,7 +105,6 @@
var colWidths = headers.ToDictionary(h => h, h => Math.Max(h.Length,
rows.Any() ? rows.Max(r => r.GetValueOrDefault(h, "").Length) : 0));

// Header
Console.WriteLine(string.Join(" | ", headers.Select(h => h.PadRight(colWidths[h]))));
Console.WriteLine(string.Join("-|-", headers.Select(h => new string('-', colWidths[h]))));

Expand All @@ -105,3 +114,70 @@
headers.Select(h => (row.GetValueOrDefault(h, "")).PadRight(colWidths[h]))));
}
}

static void QueryTable(string[] headers, List<Dictionary<string, string>> records, string sql)
{
// Simple SQL-like: "SELECT col1, col2 WHERE col3 = 'value' ORDER BY col1 LIMIT 10"
var selectMatch = Regex.Match(sql, @"SELECT\s+(.+?)(?:\s+WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(\w+)(?:\s+(ASC|DESC))?)?(?:\s+LIMIT\s+(\d+))?", RegexOptions.IgnoreCase);

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'RegexOptions' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'RegexOptions' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'RegexOptions' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'RegexOptions' does not exist in the current context

Check failure on line 121 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context
if (!selectMatch.Success) { Console.Error.WriteLine("Invalid query syntax."); return; }

var selectCols = selectMatch.Groups[1].Value == "*"
? headers.ToList()
: selectMatch.Groups[1].Value.Split(',').Select(c => c.Trim()).Where(h => headers.Contains(h)).ToList();

var whereClause = selectMatch.Groups[2].Value;
var orderBy = selectMatch.Groups[3].Value;
var orderDir = selectMatch.Groups[4].Success ? selectMatch.Groups[4].Value : "ASC";
var limit = selectMatch.Groups[5].Success ? int.Parse(selectMatch.Groups[5].Value) : int.MaxValue;

var filtered = records.AsEnumerable();
if (!string.IsNullOrEmpty(whereClause))
{
var whereMatch = Regex.Match(whereClause, @"(\w+)\s*(=|!=|>|<|>=|<=)\s*'?(.+?)'?$");

Check failure on line 136 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context

Check failure on line 136 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context

Check failure on line 136 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context

Check failure on line 136 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

The name 'Regex' does not exist in the current context
if (whereMatch.Success)
{
var col = whereMatch.Groups[1].Value;
var op = whereMatch.Groups[2].Value;
var val = whereMatch.Groups[3].Value.Trim('\'');
filtered = filtered.Where(r =>
{
if (!r.ContainsKey(col)) return false;
var rv = r[col];
return op switch
{
"=" => rv == val,
"!=" => rv != val,
">" => string.Compare(rv, val, StringComparison.Ordinal) > 0,
"<" => string.Compare(rv, val, StringComparison.Ordinal) < 0,
_ => false
};
});
}
}

if (!string.IsNullOrEmpty(orderBy) && headers.Contains(orderBy))
{
filtered = orderDir == "DESC"
? filtered.OrderByDescending(r => r.GetValueOrDefault(orderBy, ""))
: filtered.OrderBy(r => r.GetValueOrDefault(orderBy, ""));
}

var result = filtered.Take(limit).ToList();
PrintTable(selectCols.ToArray(), result);

Check failure on line 166 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

A static local function cannot contain a reference to 'this' or 'base'.

Check failure on line 166 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

A static local function cannot contain a reference to 'this' or 'base'.

Check failure on line 166 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

A static local function cannot contain a reference to 'this' or 'base'.

Check failure on line 166 in CsvSharp/Program.cs

View workflow job for this annotation

GitHub Actions / build

A static local function cannot contain a reference to 'this' or 'base'.
Console.WriteLine($"\n{result.Count} row(s)");
}

static void ShowStats(string[] headers, List<Dictionary<string, string>> records)
{
Console.WriteLine($"Rows: {records.Count}");
Console.WriteLine($"Columns: {headers.Length}");
Console.WriteLine($"\nColumn stats:");
foreach (var h in headers)
{
var values = records.Select(r => r.GetValueOrDefault(h, "")).Where(v => !string.IsNullOrEmpty(v)).ToList();
var nums = values.Select(v => { double.TryParse(v, out var n); return (ok: double.TryParse(v, out n), val: n); })
.Where(x => x.ok).Select(x => x.val).ToList();
Console.WriteLine($" {h}: {values.Count} non-empty" +
(nums.Any() ? $", min={nums.Min():F2}, max={nums.Max():F2}, avg={nums.Average():F2}" : ""));
}
}
Loading