From 1cbe4f90fe700ec1a49a4bea0a7a56dde0641e3f Mon Sep 17 00:00:00 2001 From: Abir Khan Date: Wed, 5 Aug 2026 17:50:37 +0800 Subject: [PATCH] feat: SQL-like query, column statistics --- CsvSharp/Program.cs | 78 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/CsvSharp/Program.cs b/CsvSharp/Program.cs index 80e5f7d..7ba2d80 100644 --- a/CsvSharp/Program.cs +++ b/CsvSharp/Program.cs @@ -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 ''"); return 1; } + QueryTable(headers, records, sql); + break; + + case "stats": + ShowStats(headers, records); + break; + default: Console.Error.WriteLine($"Unknown command: {command}"); return 1; @@ -95,7 +105,6 @@ static void PrintTable(string[] headers, IEnumerable> 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])))); @@ -105,3 +114,70 @@ static void PrintTable(string[] headers, IEnumerable> headers.Select(h => (row.GetValueOrDefault(h, "")).PadRight(colWidths[h])))); } } + +static void QueryTable(string[] headers, List> 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); + 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*'?(.+?)'?$"); + 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); + Console.WriteLine($"\n{result.Count} row(s)"); +} + +static void ShowStats(string[] headers, List> 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}" : "")); + } +}