Skip to content

Commit

Permalink
Allow Boolean encoding to be governed by new BoolEncodingStrategy (#10)
Browse files Browse the repository at this point in the history
* Add boolean encoding strategy support

This adds support for encoding boolean values in a variety of ways:

- true/false (the default)
- TRUE/FALSE
- yes/no
- YES/NO
- 1/0
- Custom user-supplied strings

* Simplify boolean encoding

Rather than having a switch statement with identical ternary checks,
move the true/false values to be computed properties on the strategy
enum, which then simplifies the encode function.
  • Loading branch information
scottmatthewman authored Jan 3, 2024
1 parent 4d87e49 commit d810868
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 6 deletions.
4 changes: 3 additions & 1 deletion Sources/SwiftCSVEncoder/CSVEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
51 changes: 48 additions & 3 deletions Sources/SwiftCSVEncoder/CSVEncoderConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 74 additions & 2 deletions Tests/SwiftCSVEncoderTests/CSVEncodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "☠️"))), "☠️")
}
}

0 comments on commit d810868

Please sign in to comment.