diff --git a/Sources/SwiftCSVEncoder/CSVEncodable.swift b/Sources/SwiftCSVEncoder/CSVEncodable.swift index 2325f14..716ecf2 100644 --- a/Sources/SwiftCSVEncoder/CSVEncodable.swift +++ b/Sources/SwiftCSVEncoder/CSVEncodable.swift @@ -82,7 +82,9 @@ extension Double: CSVEncodable { extension Bool: CSVEncodable { public func encode(configuration: CSVEncoderConfiguration) -> String { - self == true ? "true" : "false" + let (trueValue, falseValue) = configuration.boolEncodingStrategy.encodingValues + + return self == true ? trueValue : falseValue } } diff --git a/Sources/SwiftCSVEncoder/CSVEncoderConfiguration.swift b/Sources/SwiftCSVEncoder/CSVEncoderConfiguration.swift index 3a67c51..494db15 100644 --- a/Sources/SwiftCSVEncoder/CSVEncoderConfiguration.swift +++ b/Sources/SwiftCSVEncoder/CSVEncoderConfiguration.swift @@ -16,10 +16,20 @@ public struct CSVEncoderConfiguration { /// The default strategy is the ``DateEncodingStrategy-swift.enum/iso8601`` strategy. public private(set) var dateEncodingStrategy: DateEncodingStrategy = .iso8601 + /// The strategy to use when encoding Boolean values. + /// + /// The default strategy is the ``BoolEncodingStrategy-swift.enum/trueFalse`` strategy. + public private(set) var boolEncodingStrategy: BoolEncodingStrategy = .trueFalse + /// Creates a new instance of ``CSVEncoderConfiguration`` with the requisite configuration values /// - Parameter dateEncodingStrategy: The strategy to use when encoding dates - public init(dateEncodingStrategy: DateEncodingStrategy) { + /// - Parameter boolEncodingStrategy: The strategy to use when encoding Boolean values + public init( + dateEncodingStrategy: DateEncodingStrategy = .iso8601, + boolEncodingStrategy: BoolEncodingStrategy = .trueFalse + ) { self.dateEncodingStrategy = dateEncodingStrategy + self.boolEncodingStrategy = boolEncodingStrategy } /// The strategy to use when encoding `Date` objects for CSV output. @@ -34,10 +44,45 @@ public struct CSVEncoderConfiguration { /// - Parameter custom: A closure that receives the `Date` to encode, and returns the `String` to include in the CSV output. case custom(@Sendable (Date) -> String) } - + + /// The strategy to use when encoding `Bool` objects for CSV output. + public enum BoolEncodingStrategy { + /// The strategy that emits `true` and `false` for Boolean fields + case trueFalse + /// The strategy that emits `TRUE` and `FALSE` for Boolean fields + case trueFalseUppercase + /// The strategy that emite `yes` and `no` for Boolean fields + case yesNo + /// The strategy that emits `YES` and `NO` for Boolean fields + case yesNoUppercase + /// The strategy that emits `1` and `0` for Boolean fields + case integer + /// A custom strategy that emitss the custom supplied strings for Boolean fields + case custom(true: String, false: String) + } + /// A default set of configuration values. /// /// This configuration set will be used when a ``CSVTable`` is initialized with setting a custom /// configuration. - public static var `default`: CSVEncoderConfiguration = .init(dateEncodingStrategy: .iso8601) + public static var `default`: CSVEncoderConfiguration = CSVEncoderConfiguration() +} + +internal extension CSVEncoderConfiguration.BoolEncodingStrategy { + var encodingValues: (String, String) { + switch self { + case .trueFalse: + return ("true", "false") + case .trueFalseUppercase: + return ("TRUE", "FALSE") + case .yesNo: + return ("yes", "no") + case .yesNoUppercase: + return ("YES", "NO") + case .integer: + return ("1", "0") + case .custom(let trueValue, let falseValue): + return (trueValue, falseValue) + } + } } diff --git a/Sources/SwiftCSVEncoder/SwiftCSVEncoder.docc/SwiftCSVEncoder.md b/Sources/SwiftCSVEncoder/SwiftCSVEncoder.docc/SwiftCSVEncoder.md index 299395e..b8a31bf 100644 --- a/Sources/SwiftCSVEncoder/SwiftCSVEncoder.docc/SwiftCSVEncoder.md +++ b/Sources/SwiftCSVEncoder/SwiftCSVEncoder.docc/SwiftCSVEncoder.md @@ -52,6 +52,8 @@ CSVColumn("Description", \.description) ``SwiftCSVEncoder`` adds ``CSVEncodable`` conformance to the Swift primitives `String`, `Int`, `Double`, `Bool` and Foundation data types `Date` and `UUID`. Optional forms are automatically handled, with `nil` values being output as empty cells. +``CSVTable/init(columns:configuration:)`` optionally takes a `configuration:` object that specifies strategies for converting `Date` and `Bool` values into strings. This can be essential for some CSV importers which expect columns of those types to be in a specific format in order to correctly recognise them. + To generate the CSV file, call ``CSVTable/export(rows:)``. The return value is the full CSV file, including a header row. String items will be enclosed in double quotes where needed: ```csv diff --git a/Tests/SwiftCSVEncoderTests/CSVEncodableTests.swift b/Tests/SwiftCSVEncoderTests/CSVEncodableTests.swift index d044b39..278b00e 100644 --- a/Tests/SwiftCSVEncoderTests/CSVEncodableTests.swift +++ b/Tests/SwiftCSVEncoderTests/CSVEncodableTests.swift @@ -74,15 +74,87 @@ final class CSVEncodableTests: XCTestCase { XCTAssertEqual(date.encode(configuration: configuration), "Custom returned value") } - func testBoolTrueEncodedAsString() { + func testBoolTrueEncodedAsStringByDefault() { let input: Bool = true XCTAssertEqual(input.encode(configuration: .default), "true") } - func testBoolFalseEncodedAsString() { + func testBoolFalseEncodedAsStringByDefault() { let input: Bool = false XCTAssertEqual(input.encode(configuration: .default), "false") } + + func testBoolTrueEncodedAsStringByTrueFalse() { + let input: Bool = true + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .trueFalse)), "true") + } + + func testBoolFalseEncodedAsStringByTrueFalse() { + let input: Bool = false + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .trueFalse)), "false") + } + + func testBoolTrueEncodedAsStringByTrueFalseUppercase() { + let input: Bool = true + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .trueFalseUppercase)), "TRUE") + } + + func testBoolFalseEncodedAsStringByTrueFalseUppercase() { + let input: Bool = false + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .trueFalseUppercase)), "FALSE") + } + + func testBoolTrueEncodedAsStringByYesNo() { + let input: Bool = true + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .yesNo)), "yes") + } + + func testBoolFalseEncodedAsStringByYesNo() { + let input: Bool = false + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .yesNo)), "no") + } + + func testBoolTrueEncodedAsStringByYesNoUppercase() { + let input: Bool = true + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .yesNoUppercase)), "YES") + } + + func testBoolFalseEncodedAsStringByYesNoUppercase() { + let input: Bool = false + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .yesNoUppercase)), "NO") + } + + func testBoolTrueEncodedAsStringByInteger() { + let input: Bool = true + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .integer)), "1") + } + + func testBoolFalseEncodedAsStringByInteger() { + let input: Bool = false + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .integer)), "0") + } + + func testBoolTrueEncodedAsCustom() { + let input: Bool = true + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .custom(true: "❤️", false: "☠️"))), "❤️") + } + + func testBoolFalseEncodedAsCustom() { + let input: Bool = false + + XCTAssertEqual(input.encode(configuration: CSVEncoderConfiguration(boolEncodingStrategy: .custom(true: "❤️", false: "☠️"))), "☠️") + } }