Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "table",
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.1"),
// .package(url: "https://github.com/groue/GRMustache.swift", from: "4.0.0")
],
targets: [
Expand Down
7 changes: 5 additions & 2 deletions Sources/table/Cell.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Cell {
private var computedValue: String?
private let computeValue: () -> String
public let type: CellType

public var value: String {
get {
Expand All @@ -15,13 +16,15 @@ class Cell {

public var description: String { return value }

init(value: String) {
init(value: String, type: CellType = .string) {
self.computedValue = value
self.computeValue = { value }
self.type = type
}

init(fn: @escaping () -> String) {
init(fn: @escaping () -> String, type: CellType = .string) {
self.computeValue = fn
self.computedValue = nil
self.type = type
}
}
94 changes: 94 additions & 0 deletions Sources/table/CellType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Foundation

enum CellType {
case string
case number
case date
case boolean

static func fromString(_ type: String) throws -> CellType {
switch type.trimmingCharacters(in: .whitespaces).lowercased() {
case "string": return .string
case "number": return .number
case "date": return .date
case "boolean": return .boolean
default: throw RuntimeError("Unsupported cell type \(type)")
}
}

static func fromStringList(_ types: String) throws -> [CellType] {
do {
// long format "string, number, date, boolean"
return try types.split(separator: ",") .map { try CellType.fromString(String($0.trimmingCharacters(in: .whitespaces))) }
} catch {
do {
// short format "sndb" for string, number, date, boolean
return try types.trimmingCharacters(in: .whitespaces).lowercased().map { c in
switch c {
case "s": return .string
case "n": return .number
case "d": return .date
case "b": return .boolean
default: throw RuntimeError("Unsupported cell type \(c)")
}
}
} catch {
throw RuntimeError("Unsupported cell type \(types)")
}
}
}

// Infers cell types from the first few rows of data
// TODO: handle more complex cases when the first row is null
static func infer(rows: [[String]]) -> [CellType] {
let dateFormat = DateFormatter()
dateFormat.dateFormat = "yyyy-MM-dd hh:mm:ss"

// Infer cell types from the first row
var types: [CellType] = rows.first?.map { value in
if value.isNumber {
return .number
} else if value.isDate {
return .date
} else if value.isBoolean {
return .boolean
} else {
return .string
}
} ?? []

// refine with the rest of the rows
for row in rows.dropFirst() {
for (idx, value) in row.enumerated() {
let type = types[idx]

if value.caseInsensitiveCompare("null") == .orderedSame {
continue // skip null values as they don't affect type inference
}

if type == .number && !value.isNumber{
types[idx] = .string
} else if type == .date && !value.isDate {
types[idx] = .string
} else if type == .boolean && !value.isBoolean {
types[idx] = .string
}
}
}

debug("Infered cell types: \(CellType.toString(types))")

return types
}

static func toString(_ types: [CellType]) -> String {
return types.map { type in
switch type {
case .string: return "string"
case .number: return "number"
case .date: return "date"
case .boolean: return "boolean"
}
}.joined(separator: ", ")
}
}
17 changes: 16 additions & 1 deletion Sources/table/Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

extension Optional {
func orThrow(_ errorExpression: @autoclosure () -> Error) throws -> Wrapped {
switch self {
Expand All @@ -14,10 +16,23 @@ extension String {
return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil
}

// TODO: remove me
var isNumber: Bool {
return self.matches("^-?[0-9]*$")
if let _ = Double(self) {
return true
}
return false
}

var isDate: Bool {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" // Adjust as needed for your date format
return dateFormatter.date(from: self.replacingOccurrences(of: "T", with: " ")) != nil
}

var isBoolean: Bool {
return self.caseInsensitiveCompare("true") == .orderedSame || self.caseInsensitiveCompare("false") == .orderedSame
}
}

extension Array {
Expand Down
20 changes: 9 additions & 11 deletions Sources/table/Format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,15 @@ class Format {
}

if name == "%quoted_values" {
return row.components.map {
let v = $0.value
if v.caseInsensitiveCompare("true") == .orderedSame ||
v.caseInsensitiveCompare("false") == .orderedSame ||
v.caseInsensitiveCompare("null") == .orderedSame ||
v.isNumber
{
return v
} else {
return "'\(v)'"
}
return row.components.enumerated().map { (index, cell) in
let v = cell.value
let type = row.header?.type(ofIndex: index) ?? .string

if type == .boolean || type == .number || v.caseInsensitiveCompare("null") == .orderedSame {
return v
} else {
return "'\(v)'"
}
}.joined(separator: ",")
}

Expand Down
28 changes: 22 additions & 6 deletions Sources/table/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import Foundation

class Header {
let cols: [String]
let size: Int
let size: Int
let types: [CellType]

convenience init(data: String, delimeter: String, trim: Bool, hasOuterBorders: Bool) throws {
convenience init(data: String, delimeter: String, trim: Bool, hasOuterBorders: Bool, types: [CellType]? = nil) throws {
var components = try Csv.parseLine(data, delimeter: delimeter)

if trim {
Expand All @@ -15,16 +16,18 @@ class Header {
components = components.dropFirst().dropLast()
}

self.init(components: components)
self.init(components: components, types: types ?? Array(repeating: .string, count: components.count))
}

init(components: [String]) {
init(components: [String], types: [CellType]) {
cols = components
size = components.count
self.types = types
}

static func auto(size: Int) -> Header {
Header(components: stride(from: 0, to: size, by: 1).map { idx in "col\(idx)" })
let components = stride(from: 0, to: size, by: 1).map { idx in "col\(idx)" }
return Header(components: components, types: Array(repeating: .string, count: size))
}

subscript(index: Int) -> String {
Expand All @@ -39,13 +42,26 @@ class Header {
cols.firstIndex(of: ofColumn)
}

func type(ofColumn: String) -> CellType? {
guard let index = index(ofColumn: ofColumn) else { return .string }
return index < types.count ? types[index] : .string
}

func type(ofIndex: Int) -> CellType? {
return ofIndex < types.count ? types[ofIndex] : .string
}

func components() -> [String] {
cols
}

func withTypes(_ types: [CellType]) -> Header {
Header(components: cols, types: types)
}
}

extension Header {
static func +(h1: Header, h2: Header) -> Header {
Header(components: h1.components() + h2.components())
Header(components: h1.components() + h2.components(), types: h1.types + h2.types)
}
}
2 changes: 1 addition & 1 deletion Sources/table/Join.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class Join {


static func parse(_ file: String, joinOn: String?, firstTable: any Table) throws -> Join {
try parse(try ParsedTable.parse(path: file, hasHeader: nil, headerOverride: nil, delimeter: nil), joinOn: joinOn, firstTable: firstTable)
try parse(try ParsedTable.parse(path: file, hasHeader: nil, headerOverride: nil, delimeter: nil, userTypes: nil), joinOn: joinOn, firstTable: firstTable)
}

static func parse(_ matchTable: ParsedTable, joinOn: String?, firstTable: any Table) throws -> Join {
Expand Down
7 changes: 6 additions & 1 deletion Sources/table/MainApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ struct MainApp: ParsableCommand {
@Option(name: .customLong("header"), help: "Override header. Columns should be specified separated by comma.")
var header: String?

@Option(name: .customLong("types"), help: "Optionally specify column types explicitly. If not set, the tool will try to detect types automatically. Example: --types string,number,date or in short form . Supported types: string, number, date, boolean.")
var columnTypes: String?

@Option(name: .customLong("columns"), help: "Speficies a comma separated list of columns to show in the output. Not compatible with --print.")
var columns: String?

Expand Down Expand Up @@ -112,9 +115,11 @@ struct MainApp: ParsableCommand {
print("Debug enabled")
}

let userTypes = try columnTypes.map { try CellType.fromStringList($0) }

let headerOverride = header.map { try! Header(data: $0, delimeter: ",", trim: false, hasOuterBorders: false) }

var table: any Table = try ParsedTable.parse(path: inputFile, hasHeader: !noInHeader, headerOverride: headerOverride, delimeter: delimeter)
var table: any Table = try ParsedTable.parse(path: inputFile, hasHeader: !noInHeader, headerOverride: headerOverride, delimeter: delimeter, userTypes: userTypes)

let filter = try filter.map { try Filter.compile(filter: $0, header: table.header) }

Expand Down
21 changes: 4 additions & 17 deletions Sources/table/Row.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,12 @@ class Row {
let components: [Cell]
let header: Header?

convenience init(header: Header?, index: Int, data: String, delimeter: String, trim: Bool, hasOuterBorders: Bool) {
var components = data.components(separatedBy: delimeter)

if trim {
components = components.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
}

if hasOuterBorders {
components = components.dropFirst().dropLast()
}

self.init(header: header, index: index, components: components)
}

convenience init(header: Header?, index: Int, components: [String]) {
self.init(header: header, index: index, cells: components.map { Cell(value: $0) })
convenience init(header: Header, index: Int, components: [String]) {
let components = zip(components, header.types).map { Cell(value: $0.0, type: $0.1) }
self.init(header: header, index: index, cells: components)
}

init(header: Header?, index: Int, cells: [Cell]) {
init(header: Header, index: Int, cells: [Cell]) {
self.header = header
self.index = index
self.components = cells
Expand Down
Loading