From af444c432739d533c1645b98e7ac7f7c5ad4e030 Mon Sep 17 00:00:00 2001 From: Sergey Khruschak Date: Fri, 21 Nov 2025 21:04:03 +0200 Subject: [PATCH] Diff functionality added --- README.md | 25 +- Sources/table/Diff.swift | 103 ++++ Sources/table/MainApp.swift | 15 +- Sources/table/TableView.swift | 121 +++++ Tests/table-Tests/DiffTests.swift | 479 ++++++++++++++++++ .../NewColumnsTableViewTests.swift | 28 +- 6 files changed, 753 insertions(+), 18 deletions(-) create mode 100644 Sources/table/Diff.swift create mode 100644 Tests/table-Tests/DiffTests.swift diff --git a/README.md b/README.md index fdd91d4..505883f 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,7 @@ Steve Pitt * Showing only distinct data for a column * Sorting by the specified columns * Data generation - -## Planned Features -* Reduce functions +* Diffing two tables by the specified columns # Installation @@ -119,6 +117,21 @@ table file1.csv --join file2.csv --on 'id=product_id' table ./test-data/table-format.out --sort "!available,id" ``` +* Diff two tables by the 'id' column: + +```bash +table file1.csv --diff file2.csv --on 'id=id' +``` +Produces: + +``` +╭────┬────────────┬───────────┬───────────╮ +│ id │ first_name │ last_name │ _source │ +├────┼────────────┼───────────┼───────────┤ +│ 2 │ Mary │ McAdams │ left │ +╰────┴────────────┴───────────┴───────────╯ +``` + * Generate table with test data ```bash @@ -150,6 +163,12 @@ INSERT INTO orders (id, amound, status) VALUES ('64FC986A-93A1-4579-B7F5-896CD77 INSERT INTO orders (id, amound, status) VALUES ('74CB99C8-D23F-4081-901B-8634187E4269', 529, 'ok'); ``` +* For working with JSONL files (one JSON object per line) it can be combined with the `jq` tool (no nesting yet): + +```bash +cat objects.jsonl | jq --slurp -r '(map(keys) | add | unique) as $cols | map(. as $row | $cols | map($row[.])) as $rows | $cols, $rows[] | @csv' | table +``` + ## Building from source ```bash diff --git a/Sources/table/Diff.swift b/Sources/table/Diff.swift new file mode 100644 index 0000000..d70d04a --- /dev/null +++ b/Sources/table/Diff.swift @@ -0,0 +1,103 @@ +import Foundation + +enum DiffMode: String { + case left = "left" + case right = "right" + case both = "both" + + static func fromString(_ str: String) throws -> DiffMode { + guard let mode = DiffMode(rawValue: str.lowercased()) else { + throw RuntimeError("Invalid diff mode: \(str). Supported modes: left, right, both. Default is both.") + } + return mode + } +} + +class Diff { + let firstColumn: String + let secondColumn: String + let secondColIndex: Int + let matchTable: ParsedTable + let mode: DiffMode + + var loaded: Bool = false + var rowsCache: Set = [] + // Store all rows from second table for right/both modes + var secondTableRows: [Row] = [] + + init(firstColumn: String, secondColumn: String, matchTable: ParsedTable, mode: DiffMode) throws { + self.firstColumn = firstColumn + self.secondColumn = secondColumn + self.matchTable = matchTable + self.mode = mode + self.secondColIndex = try matchTable.header.index(ofColumn: secondColumn).orThrow(RuntimeError("Column \(secondColumn) is not found in second table")) + + debug("Diffing tables on columns \(firstColumn)=\(secondColumn) with mode: \(mode.rawValue)") + } + + func exists(row: Row) -> Bool { + guard let columnValue = row[firstColumn] else { + return false + } + return rowsCache.contains(columnValue) + } + + func load() throws -> Diff { + while let r = try matchTable.next() { + let colValue = r[secondColIndex] + rowsCache.insert(colValue) + // Store rows for right/both modes + if mode == .right || mode == .both { + secondTableRows.append(r) + } + } + + loaded = true + debug("Loaded \(rowsCache.count) rows from second table for diff") + + return self + } + + static func parse(_ file: String, diffOn: String?, noInHeader: Bool, firstTable: any Table, mode: String?) throws -> Diff { + let matchTable = try ParsedTable.parse(path: file, hasHeader: !noInHeader, headerOverride: nil, delimeter: nil, userTypes: nil) + return try parse(matchTable, diffOn: diffOn, firstTable: firstTable, mode: mode) + } + + static func parse(_ matchTable: ParsedTable, diffOn: String?, firstTable: any Table, mode: String?) throws -> Diff { + let (first, second) = try diffOn.map { diffExpr in + let components = diffExpr.components(separatedBy: "=") + + if components.count != 2 { + throw RuntimeError("Diff expression should have format: table1_column=table2_column") + } + + let firstCol = components[0].trimmingCharacters(in: .whitespacesAndNewlines) + let secondCol = components[1].trimmingCharacters(in: .whitespacesAndNewlines) + + // Validate columns exist + if firstTable.header.index(ofColumn: firstCol) == nil { + throw RuntimeError("Column \(firstCol) is not found in first table") + } + + if matchTable.header.index(ofColumn: secondCol) == nil { + throw RuntimeError("Column \(secondCol) is not found in second table") + } + + return (firstCol, secondCol) + } ?? { + let firstCol = firstTable.header[0] + let secondCol = matchTable.header[0] + return (firstCol, secondCol) + }() + + let diffMode = try mode.map { try DiffMode.fromString($0) } ?? .both + + return try Diff( + firstColumn: first, + secondColumn: second, + matchTable: matchTable, + mode: diffMode + ).load() + } +} + diff --git a/Sources/table/MainApp.swift b/Sources/table/MainApp.swift index 826ee74..a937d6e 100644 --- a/Sources/table/MainApp.swift +++ b/Sources/table/MainApp.swift @@ -141,9 +141,15 @@ struct MainApp: AsyncParsableCommand { @Option(name: .customLong("join"), help: "Speficies a second file path to join with the current one. Joining column is the first one for both tables or can be specified by the --on option.") var joinFile: String? - @Option(name: .customLong("on"), help: "Speficies column names to join on. Requires --join option. Syntax {table1 column}={table 2 column}. Example: --on city_id=id.") + @Option(name: .customLong("on"), help: "Speficies column names to join or diff on. Requires --join or --diff option. Syntax {table1 column}={table 2 column}. Example: --on city_id=id.") var joinCriteria: String? + @Option(name: .customLong("diff"), help: "Specifies a second file path to diff with the current one. Shows rows in first table that don't exist in second table by default. Use --diff-mode to control behavior.") + var diffFile: String? + + @Option(name: .customLong("diff-mode"), help: "Controls diff behavior. Options: left - show rows only in first table, right - show rows only in second table, both (default) - show rows from both tables with a marker column.") + var diffMode: String? + @Option(name: .customLong("sort"), help: "Sorts output by the specified columns. Example: --sort column1,column2. Use '!' prefix to sort in descending order.") var sortColumns: String? @@ -200,6 +206,13 @@ struct MainApp: AsyncParsableCommand { table = JoinTableView(table: table, join: try Join.parse(joinFile, joinOn: joinCriteria, firstTable: table)) } + if let diffFile { + if joinFile != nil { + throw RuntimeError("--join and --diff options cannot be used together") + } + table = DiffTableView(table: table, diff: try Diff.parse(diffFile, diffOn: joinCriteria, noInHeader: noInHeader, firstTable: table, mode: diffMode)) + } + if !distinctColumns.isEmpty { try distinctColumns.forEach { if table.header.index(ofColumn: $0) == nil { throw RuntimeError("Column \($0) in distinct clause is not found in the table") } } table = DistinctTableView(table: table, distinctColumns: distinctColumns) diff --git a/Sources/table/TableView.swift b/Sources/table/TableView.swift index 731c39f..0831e8a 100644 --- a/Sources/table/TableView.swift +++ b/Sources/table/TableView.swift @@ -262,6 +262,127 @@ class SampledTableView: Table { } } +/** Table view for diffing two tables */ +class DiffTableView: Table { + var table: any Table + let diff: Diff + let header: Header + + private var memoizedTable: InMemoryTableView? + private var firstTableCache: Set? + private var filteredSecondTableRows: [Row] = [] + private var secondTableCursor: Int = 0 + private var firstTableRows: [Row] = [] + private var firstTableCursor: Int = 0 + private var firstTableExhausted: Bool = false + + init(table: any Table, diff: Diff) { + self.table = table + self.diff = diff + + if diff.mode == .both { + self.header = Header(components: ["_source"], types: [.string]) + table.header + } else { + self.header = table.header + } + + if diff.mode == .right || diff.mode == .both { + memoizedTable = table.memoized() + try? memoizedTable?.load() + buildFirstTableCache() + filterSecondTableRows() + } + } + + func next() throws -> Row? { + switch diff.mode { + case .left: + var row = try table.next() + while let curRow = row { + if !diff.exists(row: curRow) { + return curRow + } + row = try table.next() + } + return nil + + case .right: + return nextFromSecondTable() + + case .both: + if !firstTableExhausted { + while firstTableCursor < firstTableRows.count { + let curRow = firstTableRows[firstTableCursor] + firstTableCursor += 1 + + if !diff.exists(row: curRow) { + // Add marker column + let markerCell = Cell(value: "left", type: .string) + return Row( + header: header, + index: curRow.index, + cells: [markerCell] + curRow.components + ) + } + } + firstTableExhausted = true + } + + return nextFromSecondTable() + } + } + + private func buildFirstTableCache() { + guard let memoized = memoizedTable else { return } + + firstTableCache = Set() + memoized.rewind() + + while let row = memoized.next() { + if let key = row[diff.firstColumn] { + firstTableCache?.insert(key) + } + if diff.mode == .both { + firstTableRows.append(row) + } + } + + debug("DiffTableView: Loaded \(firstTableCache?.count ?? 0) rows from first table for diff") + } + + private func filterSecondTableRows() { + guard let firstCache = firstTableCache else { return } + + filteredSecondTableRows = diff.secondTableRows.filter { row in + let key = row[diff.secondColIndex] + return !firstCache.contains(key) + } + + debug("DiffTableView: Found \(filteredSecondTableRows.count) rows in the second table absent in the first") + } + + private func nextFromSecondTable() -> Row? { + guard secondTableCursor < filteredSecondTableRows.count else { + return nil + } + + let row = filteredSecondTableRows[secondTableCursor] + secondTableCursor += 1 + + if diff.mode == .both { + // Add marker column + let markerCell = Cell(value: "right", type: .string) + return Row( + header: header, + index: row.index, + cells: [markerCell] + row.components + ) + } else { + return row + } + } +} + /** Table view fully loaded into memory */ class InMemoryTableView: InMemoryTable { var table: any Table diff --git a/Tests/table-Tests/DiffTests.swift b/Tests/table-Tests/DiffTests.swift new file mode 100644 index 0000000..2d868f0 --- /dev/null +++ b/Tests/table-Tests/DiffTests.swift @@ -0,0 +1,479 @@ +import XCTest +@testable import table + +class DiffTests: XCTestCase { + + func testBasicDiffLeftMode() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["id"], "3") + XCTAssertEqual(row["name"], "Charlie") + + XCTAssertNil(try diffTable.next()) + } + + func testBasicDiffRightMode() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "right") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["id"], "3") + XCTAssertEqual(row["name"], "Charlie") + + XCTAssertNil(try diffTable.next()) + } + + func testBasicDiffBothMode() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["4", "David"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "both") + let diffTable = DiffTableView(table: table1, diff: diff) + + XCTAssertEqual(diffTable.header.components(), ["_source", "id", "name"]) + + let row1 = try diffTable.next()! + XCTAssertEqual(row1["_source"], "left") + XCTAssertEqual(row1["id"], "3") + XCTAssertEqual(row1["name"], "Charlie") + + let row2 = try diffTable.next()! + XCTAssertEqual(row2["_source"], "right") + XCTAssertEqual(row2["id"], "4") + XCTAssertEqual(row2["name"], "David") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithDefaultFirstColumns() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: nil, firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["id"], "3") + XCTAssertEqual(row["name"], "Charlie") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithCustomColumnMapping() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["user_id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "user_id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["user_id"], "3") + XCTAssertEqual(row["name"], "Charlie") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithNoDifferences() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithAllRowsDifferent() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["3", "Charlie"], + ["4", "David"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row1 = try diffTable.next()! + XCTAssertEqual(row1["id"], "1") + XCTAssertEqual(row1["name"], "Alice") + + let row2 = try diffTable.next()! + XCTAssertEqual(row2["id"], "2") + XCTAssertEqual(row2["name"], "Bob") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithEmptySecondTable() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row1 = try diffTable.next()! + XCTAssertEqual(row1["id"], "1") + XCTAssertEqual(row1["name"], "Alice") + + let row2 = try diffTable.next()! + XCTAssertEqual(row2["id"], "2") + XCTAssertEqual(row2["name"], "Bob") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithEmptyFirstTable() throws { + let table1 = ParsedTable.fromArray([], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + XCTAssertNil(try diffTable.next()) + } + + func testDiffRightModeWithEmptyFirstTable() throws { + let table1 = ParsedTable.fromArray([], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "right") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row1 = try diffTable.next()! + XCTAssertEqual(row1["id"], "1") + XCTAssertEqual(row1["name"], "Alice") + + let row2 = try diffTable.next()! + XCTAssertEqual(row2["id"], "2") + XCTAssertEqual(row2["name"], "Bob") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffBothModeWithMultipleDifferences() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"], + ["4", "David"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["3", "Charlie"], + ["5", "Eve"], + ["6", "Frank"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "both") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row1 = try diffTable.next()! + XCTAssertEqual(row1["_source"], "left") + XCTAssertEqual(row1["id"], "2") + XCTAssertEqual(row1["name"], "Bob") + + let row2 = try diffTable.next()! + XCTAssertEqual(row2["_source"], "left") + XCTAssertEqual(row2["id"], "4") + XCTAssertEqual(row2["name"], "David") + + let row3 = try diffTable.next()! + XCTAssertEqual(row3["_source"], "right") + XCTAssertEqual(row3["id"], "5") + XCTAssertEqual(row3["name"], "Eve") + + let row4 = try diffTable.next()! + XCTAssertEqual(row4["_source"], "right") + XCTAssertEqual(row4["id"], "6") + XCTAssertEqual(row4["name"], "Frank") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffThrowsErrorOnMissingColumnInFirstTable() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + XCTAssertThrowsError(try Diff.parse(table2, diffOn: "nonexistent=id", firstTable: table1, mode: "left")) { error in + let errorMessage = String(describing: error) + XCTAssertTrue(errorMessage.contains("not found") || errorMessage.contains("first table"), + "Error should mention column not found in first table, got: \(errorMessage)") + } + } + + func testDiffThrowsErrorOnMissingColumnInSecondTable() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + XCTAssertThrowsError(try Diff.parse(table2, diffOn: "id=nonexistent", firstTable: table1, mode: "left")) { error in + let errorMessage = String(describing: error) + XCTAssertTrue(errorMessage.contains("not found") || errorMessage.contains("second table"), + "Error should mention column not found in second table, got: \(errorMessage)") + } + } + + func testDiffThrowsErrorOnInvalidDiffExpression() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + // Invalid format - no equals sign + XCTAssertThrowsError(try Diff.parse(table2, diffOn: "id-id", firstTable: table1, mode: "left")) { error in + let errorMessage = String(describing: error) + XCTAssertTrue(errorMessage.contains("format"), + "Error should mention format, got: \(errorMessage)") + } + + XCTAssertThrowsError(try Diff.parse(table2, diffOn: "id=id=extra", firstTable: table1, mode: "left")) { error in + let errorMessage = String(describing: error) + XCTAssertTrue(errorMessage.contains("format"), + "Error should mention format, got: \(errorMessage)") + } + } + + func testDiffThrowsErrorOnInvalidMode() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + XCTAssertThrowsError(try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "invalid")) { error in + let errorMessage = String(describing: error) + XCTAssertTrue(errorMessage.contains("Invalid diff mode") || errorMessage.contains("invalid"), + "Error should mention invalid diff mode, got: \(errorMessage)") + } + } + + func testDiffWithMultipleColumns() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice", "25"], + ["2", "Bob", "30"], + ["3", "Charlie", "35"] + ], header: ["id", "name", "age"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice", "25"], + ["2", "Bob", "30"] + ], header: ["id", "name", "age"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["id"], "3") + XCTAssertEqual(row["name"], "Charlie") + XCTAssertEqual(row["age"], "35") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffPreservesRowIndex() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row.index, 2) + } + + func testDiffWithNumericValues() throws { + let table1 = ParsedTable.fromArray([ + ["100", "Product A"], + ["200", "Product B"], + ["300", "Product C"] + ], header: ["product_id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["100", "Product A"], + ["200", "Product B"] + ], header: ["product_id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "product_id=product_id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["product_id"], "300") + XCTAssertEqual(row["name"], "Product C") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffWithWhitespaceInColumnMapping() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: " id = id ", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["id"], "3") + XCTAssertEqual(row["name"], "Charlie") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffDefaultModeIsLeft() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"], + ["3", "Charlie"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["1", "Alice"], + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: nil) + let diffTable = DiffTableView(table: table1, diff: diff) + + let row = try diffTable.next()! + XCTAssertEqual(row["id"], "3") + XCTAssertEqual(row["name"], "Charlie") + + XCTAssertNil(try diffTable.next()) + } + + func testDiffBothModeHeader() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "both") + let diffTable = DiffTableView(table: table1, diff: diff) + + let header = diffTable.header + XCTAssertEqual(header.components(), ["_source", "id", "name"]) + XCTAssertEqual(header.size, 3) + } + + func testDiffLeftModeHeader() throws { + let table1 = ParsedTable.fromArray([ + ["1", "Alice"] + ], header: ["id", "name"]) + + let table2 = ParsedTable.fromArray([ + ["2", "Bob"] + ], header: ["id", "name"]) + + let diff = try Diff.parse(table2, diffOn: "id=id", firstTable: table1, mode: "left") + let diffTable = DiffTableView(table: table1, diff: diff) + + let header = diffTable.header + XCTAssertEqual(header.components(), ["id", "name"]) + XCTAssertEqual(header.size, 2) + } +} + diff --git a/Tests/table-Tests/NewColumnsTableViewTests.swift b/Tests/table-Tests/NewColumnsTableViewTests.swift index fe19f38..0edec46 100644 --- a/Tests/table-Tests/NewColumnsTableViewTests.swift +++ b/Tests/table-Tests/NewColumnsTableViewTests.swift @@ -10,7 +10,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name", "age"]) let format = try Format(format: "StaticValue").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("status", format)] ) @@ -38,7 +38,7 @@ class NewColumnsTableViewTests: XCTestCase { let format1 = try Format(format: "Active").validated(header: nil) let format2 = try Format(format: "Premium").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [ ("status", format1), @@ -64,7 +64,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name", "age"]) let format = try Format(format: "Name: ${name}, Age: ${age}").validated(header: table.header) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("info", format)] ) @@ -92,7 +92,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name", "age"]) let format = try Format(format: "Header: %{header()}").validated(header: table.header) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("header_info", format)] ) @@ -113,7 +113,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name", "age", "score"]) let format = try Format(format: "Sum: %{sum(${age},${score})}").validated(header: table.header) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("total", format)] ) @@ -131,7 +131,7 @@ class NewColumnsTableViewTests: XCTestCase { let table = ParsedTable.empty() let format = try Format(format: "NewColumn").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("new_col", format)] ) @@ -148,7 +148,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name"]) let format = try Format(format: "Extra").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("extra", format)] ) @@ -174,7 +174,7 @@ class NewColumnsTableViewTests: XCTestCase { let varFormat = try Format(format: "${name}").validated(header: table.header) let funcFormat = try Format(format: "%{values()}").validated(header: table.header) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [ ("static_col", staticFormat), @@ -202,7 +202,7 @@ class NewColumnsTableViewTests: XCTestCase { let format2 = try Format(format: "Second").validated(header: nil) let format3 = try Format(format: "Third").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [ ("first", format1), @@ -226,7 +226,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name"]) let format = try Format(format: "").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("empty", format)] ) @@ -241,7 +241,7 @@ class NewColumnsTableViewTests: XCTestCase { let table = ParsedTable.fromArray([["Alice"]], header: ["name"]) let format = try Format(format: " Padded ").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("padded", format)] ) @@ -256,7 +256,7 @@ class NewColumnsTableViewTests: XCTestCase { let table = ParsedTable.fromArray([["Alice"]], header: ["name"]) let format = try Format(format: "Value: $100 & 50%").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("special", format)] ) @@ -273,7 +273,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name", "age"]) let format = try Format(format: "Extra").validated(header: nil) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("extra", format)] ) @@ -292,7 +292,7 @@ class NewColumnsTableViewTests: XCTestCase { ], header: ["name", "age", "score"]) let format = try Format(format: "Max: %{max(${age},${score})}").validated(header: table.header) - var newColumnsTable = NewColumnsTableView( + let newColumnsTable = NewColumnsTableView( table: table, additionalColumns: [("max_value", format)] )