From 4ae83c6dbdf75b8e4c9afcff7967c29dfa48e4cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 02:38:06 +0000 Subject: [PATCH 01/14] Add transpose subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swaps rows and columns of NSV table data. Requires uniform row arity — exits with an error on ragged input. Works at the byte level (decode_bytes → transpose → encode_bytes). https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 56 +++++++++++++++ tests/test_transpose.sh | 149 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100755 tests/test_transpose.sh diff --git a/src/main.rs b/src/main.rs index 66dc367..a63cf85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,14 @@ enum Commands { #[arg(value_name = "FILE")] file: Option, }, + + /// Transpose rows and columns of NSV data + Transpose { + /// Input file (reads from stdin if omitted or "-") + #[arg(value_name = "FILE")] + file: Option, + }, + } fn main() { @@ -63,6 +71,9 @@ fn main() { Commands::Stats { file } => { stats(file); } + Commands::Transpose { file } => { + transpose(file); + } } } @@ -295,3 +306,48 @@ fn validate(file: Option, table: bool) -> i32 { exit_code } + +/// Transpose rows and columns of NSV data. +/// +/// Requires table input (all rows must have equal arity). +/// Errors on ragged data. +fn transpose(file: Option) { + let raw = read_input(&file); + + let start = if raw.starts_with(&[0xEF, 0xBB, 0xBF]) { 3 } else { 0 }; + let clean = match process_line_endings(&raw, start, true) { + Ok(output) => output, + Err(e) => { + eprintln!("error: {}", e); + std::process::exit(1); + } + }; + + let rows = nsv::decode_bytes(&clean); + if rows.is_empty() { + return; + } + + let min_arity = rows.iter().map(|r| r.len()).min().unwrap_or(0); + let max_arity = rows.iter().map(|r| r.len()).max().unwrap_or(0); + if min_arity != max_arity { + eprintln!( + "error: not a table — row arities vary (min {}, max {})", + min_arity, max_arity + ); + std::process::exit(1); + } + + if max_arity == 0 { + return; + } + + let transposed: Vec>> = (0..max_arity) + .map(|col| rows.iter().map(|row| row[col].clone()).collect()) + .collect(); + + io::stdout() + .write_all(&nsv::encode_bytes(&transposed)) + .unwrap_or_else(|e| panic!("write error: {}", e)); +} + diff --git a/tests/test_transpose.sh b/tests/test_transpose.sh new file mode 100755 index 0000000..cbbf8b4 --- /dev/null +++ b/tests/test_transpose.sh @@ -0,0 +1,149 @@ +#!/bin/bash + +cd "$(dirname "$0")/.." +cargo build --quiet || exit 1 + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +PASS=0 +FAIL=0 + +# run_test NAME STDIN_OR_FILE EXPECTED_STDOUT [USE_STDIN] +# Runs transpose, checks exit 0, compares stdout exactly. +run_test() { + local name="$1" + local input="$2" + local expected="$3" + local use_stdin="$4" + + local stdout stderr exit_code + + if [[ "$use_stdin" == "stdin" ]]; then + stdout=$(printf '%s' "$input" | cargo run --quiet -- transpose 2>$TMPDIR/stderr) && exit_code=0 || exit_code=$? + else + stdout=$(cargo run --quiet -- transpose "$input" 2>$TMPDIR/stderr) && exit_code=0 || exit_code=$? + fi + stderr=$(cat $TMPDIR/stderr) + rm -f $TMPDIR/stderr + + local failed=0 + + if [[ "$exit_code" -ne 0 ]]; then + echo "FAIL: $name - expected exit 0, got $exit_code" + failed=1 + fi + + if [[ -n "$stderr" ]]; then + echo "FAIL: $name - unexpected stderr: $stderr" + failed=1 + fi + + if [[ "$stdout" != "$expected" ]]; then + echo "FAIL: $name - stdout mismatch" + diff <(echo "$stdout") <(echo "$expected") + failed=1 + fi + + if [[ "$failed" -eq 0 ]]; then + echo "PASS: $name" + PASS=$((PASS + 1)) + else + FAIL=$((FAIL + 1)) + fi +} + +echo "Running transpose tests..." +echo + +F=tests/fixtures + +# ── Empty input ── + +run_test "empty file" $F/empty.nsv "" + +# ── 2×2 table → 2×2 table ── + +run_test "2x2 table" $F/table.nsv "$(printf 'a\nc\n\nb\nd\n\n')" + +# ── 3×4 table → 4×3 table ── + +run_test "3x4 table" $F/table_3x4.nsv "$(printf 'a\ne\ni\n\nb\nf\nj\n\nc\ng\nk\n\nd\nh\nl\n\n')" + +# ── Ragged rows rejected ── + +cargo run --quiet -- transpose $F/ragged.nsv >/dev/null 2>$TMPDIR/stderr && exit_code=0 || exit_code=$? +stderr=$(cat $TMPDIR/stderr) +rm -f $TMPDIR/stderr +if [[ "$exit_code" -eq 1 ]] && [[ "$stderr" == *"not a table"* ]]; then + echo "PASS: ragged rows rejected" + PASS=$((PASS + 1)) +else + echo "FAIL: ragged rows rejected - exit=$exit_code stderr=$stderr" + FAIL=$((FAIL + 1)) +fi + +# ── Single row → single column ── +# Input: one row with 3 cells [x,y,z] + +run_test "single row to column" "$(printf 'x\ny\nz\n\n')" "$(printf 'x\n\ny\n\nz\n\n')" "stdin" + +# ── Single column → single row ── +# Input: two rows each with one cell +run_test "single column to row" "$(printf 'p\n\nq\n\n')" "$(printf 'p\nq\n\n')" "stdin" + +# ── stdin input ── + +run_test "stdin" "$(printf 'a\nb\n\nc\nd\n\n')" "$(printf 'a\nc\n\nb\nd\n\n')" "stdin" + +# ── Roundtrip: transpose(transpose(x)) == x ── + +# Use table.nsv: transpose twice should recover original +first=$(cargo run --quiet -- transpose $F/table.nsv 2>/dev/null) +second=$(printf '%s' "$first" | cargo run --quiet -- transpose 2>/dev/null) +original=$(cat $F/table.nsv) +if [[ "$second" == "$original" ]]; then + echo "PASS: roundtrip transpose(transpose(x)) == x" + PASS=$((PASS + 1)) +else + echo "FAIL: roundtrip transpose(transpose(x)) == x" + diff <(echo "$second") <(echo "$original") + FAIL=$((FAIL + 1)) +fi + +# ── Roundtrip with 3×4 table ── + +first=$(cargo run --quiet -- transpose $F/table_3x4.nsv 2>/dev/null) +second=$(printf '%s' "$first" | cargo run --quiet -- transpose 2>/dev/null) +original=$(cat $F/table_3x4.nsv) +if [[ "$second" == "$original" ]]; then + echo "PASS: roundtrip 3x4" + PASS=$((PASS + 1)) +else + echo "FAIL: roundtrip 3x4" + diff <(echo "$second") <(echo "$original") + FAIL=$((FAIL + 1)) +fi + +# ── Multiline cell (escaped content preserved) ── + +stdout=$(cargo run --quiet -- transpose $F/multiline_cell.nsv 2>/dev/null) +exit_code=$? +if [[ "$exit_code" -eq 0 ]] && [[ -n "$stdout" ]]; then + # Validate the transposed output is valid NSV + echo "$stdout" | cargo run --quiet -- validate 2>/dev/null && validate_exit=0 || validate_exit=$? + if [[ "$validate_exit" -eq 0 ]]; then + echo "PASS: multiline cell transposed is valid NSV" + PASS=$((PASS + 1)) + else + echo "FAIL: multiline cell transposed is not valid NSV" + FAIL=$((FAIL + 1)) + fi +else + echo "FAIL: multiline cell transpose failed" + FAIL=$((FAIL + 1)) +fi + +echo +echo "Results: $PASS passed, $FAIL failed" +[[ "$FAIL" -eq 0 ]] From 9594516d1644921bd2cb4a161b7d9b18434e58a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 14:53:35 +0000 Subject: [PATCH 02/14] Drop silent sanitization from transpose, move instead of clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read → decode → move cells into transposed structure → encode. No intermediate copies. No silent BOM/CRLF fixup — pipe through nsv sanitize if needed. https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index a63cf85..d04ca95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -313,38 +313,31 @@ fn validate(file: Option, table: bool) -> i32 { /// Errors on ragged data. fn transpose(file: Option) { let raw = read_input(&file); - - let start = if raw.starts_with(&[0xEF, 0xBB, 0xBF]) { 3 } else { 0 }; - let clean = match process_line_endings(&raw, start, true) { - Ok(output) => output, - Err(e) => { - eprintln!("error: {}", e); - std::process::exit(1); - } - }; - - let rows = nsv::decode_bytes(&clean); + let mut rows = nsv::decode_bytes(&raw); if rows.is_empty() { return; } - let min_arity = rows.iter().map(|r| r.len()).min().unwrap_or(0); - let max_arity = rows.iter().map(|r| r.len()).max().unwrap_or(0); - if min_arity != max_arity { + let min = rows.iter().map(|r| r.len()).min().unwrap(); + let max = rows.iter().map(|r| r.len()).max().unwrap(); + if min != max { eprintln!( "error: not a table — row arities vary (min {}, max {})", - min_arity, max_arity + min, max ); std::process::exit(1); } - - if max_arity == 0 { + if max == 0 { return; } - let transposed: Vec>> = (0..max_arity) - .map(|col| rows.iter().map(|row| row[col].clone()).collect()) - .collect(); + let nrows = rows.len(); + let mut transposed: Vec>> = (0..max).map(|_| Vec::with_capacity(nrows)).collect(); + for row in rows.drain(..) { + for (c, cell) in row.into_iter().enumerate() { + transposed[c].push(cell); + } + } io::stdout() .write_all(&nsv::encode_bytes(&transposed)) From 9c563476111ff1675082b49b9ee312dd413a4746 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 14:57:41 +0000 Subject: [PATCH 03/14] Single-pass table check: compare against first row arity https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index d04ca95..130c25b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -318,21 +318,17 @@ fn transpose(file: Option) { return; } - let min = rows.iter().map(|r| r.len()).min().unwrap(); - let max = rows.iter().map(|r| r.len()).max().unwrap(); - if min != max { - eprintln!( - "error: not a table — row arities vary (min {}, max {})", - min, max - ); + let arity = rows[0].len(); + if rows.iter().any(|r| r.len() != arity) { + eprintln!("error: not a table — row arities differ"); std::process::exit(1); } - if max == 0 { + if arity == 0 { return; } let nrows = rows.len(); - let mut transposed: Vec>> = (0..max).map(|_| Vec::with_capacity(nrows)).collect(); + let mut transposed: Vec>> = (0..arity).map(|_| Vec::with_capacity(nrows)).collect(); for row in rows.drain(..) { for (c, cell) in row.into_iter().enumerate() { transposed[c].push(cell); From 3d2692a4be1c63a4d744b7f22f2536babafe151f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:02:47 +0000 Subject: [PATCH 04/14] Stream transposed rows instead of building full matrix Write each transposed row directly by iterating columns, avoiding allocation of the entire transposed structure. https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 130c25b..514ab61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -313,7 +313,7 @@ fn validate(file: Option, table: bool) -> i32 { /// Errors on ragged data. fn transpose(file: Option) { let raw = read_input(&file); - let mut rows = nsv::decode_bytes(&raw); + let rows = nsv::decode_bytes(&raw); if rows.is_empty() { return; } @@ -327,16 +327,17 @@ fn transpose(file: Option) { return; } - let nrows = rows.len(); - let mut transposed: Vec>> = (0..arity).map(|_| Vec::with_capacity(nrows)).collect(); - for row in rows.drain(..) { - for (c, cell) in row.into_iter().enumerate() { - transposed[c].push(cell); + let stdout = io::stdout(); + let mut out = stdout.lock(); + for col in 0..arity { + for row in &rows { + out.write_all(&nsv::escape_bytes(&row[col])) + .unwrap_or_else(|e| panic!("write error: {}", e)); + out.write_all(b"\n") + .unwrap_or_else(|e| panic!("write error: {}", e)); } + out.write_all(b"\n") + .unwrap_or_else(|e| panic!("write error: {}", e)); } - - io::stdout() - .write_all(&nsv::encode_bytes(&transposed)) - .unwrap_or_else(|e| panic!("write error: {}", e)); } From 96094145e008f5e19ca82f9121066c96c6c3e3f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:03:08 +0000 Subject: [PATCH 05/14] Use BufWriter for transpose output https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 514ab61..cf144f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -327,8 +327,7 @@ fn transpose(file: Option) { return; } - let stdout = io::stdout(); - let mut out = stdout.lock(); + let mut out = io::BufWriter::new(io::stdout().lock()); for col in 0..arity { for row in &rows { out.write_all(&nsv::escape_bytes(&row[col])) From b80a5b1b0c62e9d5299b49aefe865104f7f28300 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:11:15 +0000 Subject: [PATCH 06/14] Transpose on raw escaped lines, skip decode/re-encode Transpose is structure-only: it rearranges cells without modifying content. Work directly with escaped line slices instead of decoding then re-escaping each cell. https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index cf144f9..a61cd78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -313,7 +313,25 @@ fn validate(file: Option, table: bool) -> i32 { /// Errors on ragged data. fn transpose(file: Option) { let raw = read_input(&file); - let rows = nsv::decode_bytes(&raw); + + // Split into raw lines (already-escaped cell bytes). + // Group by blank lines into rows of raw cell slices. + let mut rows: Vec> = Vec::new(); + let mut current_row: Vec<&[u8]> = Vec::new(); + for line in raw.split(|&b| b == b'\n') { + if line.is_empty() { + if !current_row.is_empty() { + rows.push(current_row); + current_row = Vec::new(); + } + } else { + current_row.push(line); + } + } + if !current_row.is_empty() { + rows.push(current_row); + } + if rows.is_empty() { return; } @@ -330,7 +348,7 @@ fn transpose(file: Option) { let mut out = io::BufWriter::new(io::stdout().lock()); for col in 0..arity { for row in &rows { - out.write_all(&nsv::escape_bytes(&row[col])) + out.write_all(row[col]) .unwrap_or_else(|e| panic!("write error: {}", e)); out.write_all(b"\n") .unwrap_or_else(|e| panic!("write error: {}", e)); From 97d29de1f47a8e222aa6feaf633635199eca2e18 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:13:42 +0000 Subject: [PATCH 07/14] Use io::Result and ? for transpose writes Replace verbose unwrap_or_else(panic!) with ?, returning io::Result. Caller handles BrokenPipe silently. https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index a61cd78..8d6a0ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,12 @@ fn main() { stats(file); } Commands::Transpose { file } => { - transpose(file); + if let Err(e) = transpose(file) { + if e.kind() != io::ErrorKind::BrokenPipe { + eprintln!("write error: {}", e); + std::process::exit(1); + } + } } } } @@ -311,7 +316,7 @@ fn validate(file: Option, table: bool) -> i32 { /// /// Requires table input (all rows must have equal arity). /// Errors on ragged data. -fn transpose(file: Option) { +fn transpose(file: Option) -> io::Result<()> { let raw = read_input(&file); // Split into raw lines (already-escaped cell bytes). @@ -333,7 +338,7 @@ fn transpose(file: Option) { } if rows.is_empty() { - return; + return Ok(()); } let arity = rows[0].len(); @@ -342,19 +347,17 @@ fn transpose(file: Option) { std::process::exit(1); } if arity == 0 { - return; + return Ok(()); } let mut out = io::BufWriter::new(io::stdout().lock()); for col in 0..arity { for row in &rows { - out.write_all(row[col]) - .unwrap_or_else(|e| panic!("write error: {}", e)); - out.write_all(b"\n") - .unwrap_or_else(|e| panic!("write error: {}", e)); + out.write_all(row[col])?; + out.write_all(b"\n")?; } - out.write_all(b"\n") - .unwrap_or_else(|e| panic!("write error: {}", e)); + out.write_all(b"\n")?; } + Ok(()) } From ddbb8852dbc247a7d2f5f3439b5a3bb57abd2fbb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:17:41 +0000 Subject: [PATCH 08/14] Return error from transpose instead of calling process::exit Match the pattern used by sanitize: return Result<(), String> and let main handle printing and exit code. https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8d6a0ab..a596b27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,10 +73,8 @@ fn main() { } Commands::Transpose { file } => { if let Err(e) = transpose(file) { - if e.kind() != io::ErrorKind::BrokenPipe { - eprintln!("write error: {}", e); - std::process::exit(1); - } + eprintln!("error: {}", e); + std::process::exit(1); } } } @@ -316,7 +314,7 @@ fn validate(file: Option, table: bool) -> i32 { /// /// Requires table input (all rows must have equal arity). /// Errors on ragged data. -fn transpose(file: Option) -> io::Result<()> { +fn transpose(file: Option) -> Result<(), String> { let raw = read_input(&file); // Split into raw lines (already-escaped cell bytes). @@ -343,8 +341,7 @@ fn transpose(file: Option) -> io::Result<()> { let arity = rows[0].len(); if rows.iter().any(|r| r.len() != arity) { - eprintln!("error: not a table — row arities differ"); - std::process::exit(1); + return Err("not a table — row arities differ".to_string()); } if arity == 0 { return Ok(()); @@ -353,10 +350,10 @@ fn transpose(file: Option) -> io::Result<()> { let mut out = io::BufWriter::new(io::stdout().lock()); for col in 0..arity { for row in &rows { - out.write_all(row[col])?; - out.write_all(b"\n")?; + out.write_all(row[col]).map_err(|e| e.to_string())?; + out.write_all(b"\n").map_err(|e| e.to_string())?; } - out.write_all(b"\n")?; + out.write_all(b"\n").map_err(|e| e.to_string())?; } Ok(()) } From 94167c43cadc76f556e3aba64a642e3c33cccc45 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:26:13 +0000 Subject: [PATCH 09/14] Add transpose section to README https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index c74a17e..f6b67d9 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,20 @@ The `validate` command: - with `--table`, checks that all rows have equal length - Exits with 0 on success, 1 on warnings or errors +### Transpose + +Swap rows and columns. + +```sh +nsv transpose table.nsv +cat table.nsv | nsv transpose +``` + +The `transpose` command: +- Requires table input (all rows must have equal arity) +- Errors on ragged data +- Is its own inverse: `transpose(transpose(x)) == x` + ### Stats Structural overview of an NSV file. From d2edca83f92047e2cd869ed9cdb12671fd760c1c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:28:34 +0000 Subject: [PATCH 10/14] Fix transpose README section Drop redundant description, show involution as a pipeline. https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f6b67d9..65c2a62 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,6 @@ The `validate` command: ### Transpose -Swap rows and columns. - ```sh nsv transpose table.nsv cat table.nsv | nsv transpose @@ -61,7 +59,7 @@ cat table.nsv | nsv transpose The `transpose` command: - Requires table input (all rows must have equal arity) - Errors on ragged data -- Is its own inverse: `transpose(transpose(x)) == x` +- Is its own inverse: `nsv transpose | nsv transpose` recovers the original ### Stats From e415c2695143912bd1743af260883dbaf74d5e2c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:32:26 +0000 Subject: [PATCH 11/14] Add 't' alias for transpose subcommand https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index a596b27..8628110 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,7 @@ enum Commands { }, /// Transpose rows and columns of NSV data + #[command(alias = "t")] Transpose { /// Input file (reads from stdin if omitted or "-") #[arg(value_name = "FILE")] From 1f817105fc9b81dbe64d30971f9625dec4494b90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:33:52 +0000 Subject: [PATCH 12/14] Use 't' alias in README transpose examples https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65c2a62..3120c62 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ The `validate` command: ### Transpose ```sh -nsv transpose table.nsv -cat table.nsv | nsv transpose +nsv t table.nsv +cat table.nsv | nsv t ``` The `transpose` command: From e6e5ef4c62bb849c56f0c81d804495221e129fdc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:34:48 +0000 Subject: [PATCH 13/14] Use 't' alias consistently in README transpose section https://claude.ai/code/session_01QPwRx4hq2FgScPdegPr7ap --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3120c62..e3cfddc 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ cat table.nsv | nsv t The `transpose` command: - Requires table input (all rows must have equal arity) - Errors on ragged data -- Is its own inverse: `nsv transpose | nsv transpose` recovers the original +- Is its own inverse: `nsv t | nsv t` recovers the original ### Stats From 1c2760224d5dcad15015dac6aca8c88905262449 Mon Sep 17 00:00:00 2001 From: naming Date: Sat, 7 Mar 2026 16:39:52 +0100 Subject: [PATCH 14/14] last mile, huh --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3cfddc..8887210 100644 --- a/README.md +++ b/README.md @@ -51,15 +51,17 @@ The `validate` command: ### Transpose +Transpose a table. +Both `t` and `transpose` are valid, prefer the latter for long-living scripts. + ```sh nsv t table.nsv cat table.nsv | nsv t ``` The `transpose` command: -- Requires table input (all rows must have equal arity) -- Errors on ragged data -- Is its own inverse: `nsv t | nsv t` recovers the original +- Requires table input (all rows must have equal arity), errors on ragged data +- Allocates full input in memory (once, but still) ### Stats