├── .swift-version
├── .vscode
└── settings.json
├── Demos
├── SwiftFormatsDemo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── SwiftFormatsDemoApp.swift
│ ├── SwiftFormatsDemo.entitlements
│ ├── Support.swift
│ └── ContentView.swift
└── SwiftFormatsDemo.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── SwiftFormats.xcscheme
├── Sources
└── SwiftFormats
│ ├── IncrementalParseStrategy.swift
│ ├── FormatStyle+Extensions.swift
│ ├── FormatStyle+Debugging.swift
│ ├── FormatStyle+JSON.swift
│ ├── Support.swift
│ ├── String+Extensions.swift
│ ├── Scratch.swift
│ ├── BoolFormatStyle.swift
│ ├── DegreesMinutesSecondsNotation.swift
│ ├── FormatStyle+Hexdump.swift
│ ├── MappingFormatStyle.swift
│ ├── FormatStyle+ClosedRange.swift
│ ├── SimpleListFormatStyle.swift
│ ├── TupleFormatStyle.swift
│ ├── ParseableFormatStyle+Measurement.swift
│ ├── FormatStyle+Coordinates.swift
│ ├── FormatStyle+Vector.swift
│ ├── FormatStyle+CoreGraphics.swift
│ ├── FormatStyle+Quaternion.swift
│ ├── FormatStyle+Matrix.swift
│ ├── FormatStyle+Angles.swift
│ └── RadixedIntegerFormatStyle.swift
├── MyPlayground.playground
├── contents.xcplayground
└── Contents.swift
├── .swiftformat
├── Tests
└── SwiftFormatsTests
│ ├── CoreGraphicsTests.swift
│ ├── MatrixTests.swift
│ ├── BoolTests.swift
│ ├── VectorTests.swift
│ ├── MappingTests.swift
│ ├── AngleTests.swift
│ ├── TupleTests.swift
│ ├── SimpleListTests.swift
│ ├── QuaternionTests.swift
│ └── SwiftFormatsTests.swift
├── TestPlans
└── SwiftFormats.xctestplan
├── Package.resolved
├── .github
└── workflows
│ └── swift.yml
├── Package.swift
├── LICENSE.md
├── .gitignore
├── README.md
└── .swiftlint.yml
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.7
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Parseable",
4 | "Substrategy"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/IncrementalParseStrategy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol IncrementalParseStrategy: ParseStrategy {
4 | func incrementalParse(_ value: inout Self.ParseInput) throws -> Self.ParseOutput
5 | }
6 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/SwiftFormatsDemoApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct SwiftFormatsDemoApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/MyPlayground.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --disable andOperator
2 | --disable emptyBraces
3 | --disable fileHeader
4 | --disable redundantParens
5 | --disable trailingClosures
6 | --enable isEmpty
7 |
8 | --elseposition next-line
9 | --ifdef indent
10 | --patternlet inline
11 | --stripunusedargs closure-only
12 | --closingparen balanced
13 | --wraparguments preserve
14 | --wrapcollections before-first
15 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/SwiftFormatsDemo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension ListFormatStyle {
4 | /// Convenience method to fully create a `SimpleListFormatStyle`
5 | init(_ base: Base.Type, style: Style, width: Self.Width = .narrow, listType: Self.ListType = .and) {
6 | self = .init(memberStyle: style)
7 | self.width = width
8 | self.listType = listType
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/SwiftFormatsTests/CoreGraphicsTests.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 | import SwiftFormats
3 | import XCTest
4 |
5 | class CoreGraphicsTests: XCTestCase {
6 | func testRectangle() {
7 | XCTAssertEqual(CGRect(x: 0, y: 0, width: 0, height: 0).formatted(), "x: 0, y: 0, width: 0, height: 0")
8 | XCTAssertEqual(CGRect(x: 1, y: 2, width: 3, height: 4).formatted(), "x: 1, y: 2, width: 3, height: 4")
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/TestPlans/SwiftFormats.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "3A4C2ACE-27CE-4F8C-B93B-0EC578912072",
5 | "name" : "Test Scheme Action",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 |
13 | },
14 | "testTargets" : [
15 | {
16 | "target" : {
17 | "containerPath" : "container:",
18 | "identifier" : "SwiftFormatsTests",
19 | "name" : "SwiftFormatsTests"
20 | }
21 | }
22 | ],
23 | "version" : 1
24 | }
25 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-algorithms",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-algorithms",
7 | "state" : {
8 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e",
9 | "version" : "1.0.0"
10 | }
11 | },
12 | {
13 | "identity" : "swift-numerics",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/apple/swift-numerics",
16 | "state" : {
17 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
18 | "version" : "1.0.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Swift project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
3 |
4 | name: Swift
5 |
6 | on:
7 | push:
8 | pull_request:
9 |
10 | jobs:
11 | build:
12 | runs-on: macos-15
13 | steps:
14 | - uses: maxim-lobanov/setup-xcode@v1
15 | with:
16 | xcode-version: 16
17 | - uses: actions/checkout@v3
18 | - name: Build
19 | run: swift build -v
20 | - name: Run tests
21 | run: swift test -v
22 | - name: Build Demo
23 | run: cd Demos && xcodebuild -scheme 'SwiftFormatsDemo' -sdk iphonesimulator build
24 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-algorithms",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-algorithms",
7 | "state" : {
8 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e",
9 | "version" : "1.0.0"
10 | }
11 | },
12 | {
13 | "identity" : "swift-numerics",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/apple/swift-numerics",
16 | "state" : {
17 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
18 | "version" : "1.0.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/SwiftFormatsTests/MatrixTests.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 | import Foundation
3 | @testable import SwiftFormats
4 | import XCTest
5 | import simd
6 |
7 | private let locale = Locale(identifier: "en_US")
8 |
9 | class MatrixTests: XCTestCase {
10 | func test1() throws {
11 | let matrix = simd_float4x4(rows: [
12 | [0, 1, 2, 3],
13 | [4, 5, 6, 7],
14 | [8, 9, 10, 11],
15 | [12, 13, 14, 15],
16 | ])
17 | let string = "0, 1, 2, 3\n4, 5, 6, 7\n8, 9, 10, 11\n12, 13, 14, 15"
18 | XCTAssertEqual("\(matrix, format: .matrix)", string)
19 | XCTAssertEqual(try MatrixParseStrategy(scalarStrategy: FloatingPointFormatStyle.number.parseStrategy).parse(string), matrix)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/SwiftFormatsTests/BoolTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftFormats
3 | import XCTest
4 |
5 | class BoolValueTests: XCTestCase {
6 | func testFormatting() {
7 | XCTAssertEqual(true.formatted(), "true")
8 | XCTAssertEqual(false.formatted(), "false")
9 | XCTAssertEqual(true.formatted(.bool), "true")
10 | XCTAssertEqual(false.formatted(.bool), "false")
11 | XCTAssertEqual(true.formatted(.bool.true("YES")), "YES")
12 | XCTAssertEqual(false.formatted(.bool.false("NO")), "NO")
13 | }
14 |
15 | func testParsing() {
16 | XCTAssertEqual(try BoolParseStrategy().parse("true"), true)
17 | XCTAssertEqual(try BoolParseStrategy().parse("false"), false)
18 | XCTAssertThrowsError(try BoolParseStrategy().parse("aardvark"))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/SwiftFormatsTests/VectorTests.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 | import Foundation
3 | @testable import SwiftFormats
4 | import XCTest
5 | import simd
6 |
7 | private let locale = Locale(identifier: "en_US")
8 |
9 | class VectorTests: XCTestCase {
10 | func test1() throws {
11 | let vector = SIMD3(0, 1, 2)
12 | // XCTAssertEqual("\(vector, format: .simd())", "x: 0, y: 1, z: 2")
13 | // XCTAssertEqual("\(vector, format: .simd(mappingStyle: false))", "0, 1, 2")
14 | // XCTAssertEqual(try SIMDParseStrategy(scalarStrategy: FloatingPointFormatStyle.number.parseStrategy, mappingStyle: false).parse("0, 1, 2"), vector)
15 | XCTAssertEqual(try VectorParseStrategy(scalarStrategy: FloatingPointFormatStyle.number.parseStrategy, compositeStyle: .mapping).parse("x: 0, y: 1, z: 2"), vector)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/SwiftFormatsTests/MappingTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import SwiftFormats
3 | import XCTest
4 |
5 | class MappingTests: XCTestCase {
6 | func test1() {
7 | let style = MappingFormatStyle(keyType: Int.self, valueType: Int.self, keyStyle: .number, valueStyle: .number)
8 | XCTAssertEqual(style.format([(1, 10), (2, 20)]), "1: 10, 2: 20")
9 | let parser = style.parseStrategy
10 | XCTAssertEqual(Dictionary(uniqueKeysWithValues: try parser.parse("1:10, 2:20")), [1: 10, 2: 20])
11 | }
12 |
13 | func test2() {
14 | let style = MappingFormatStyle(keyType: String.self, valueType: Int.self, keyStyle: IdentityFormatStyle(), valueStyle: .number)
15 | XCTAssertEqual(style.format([("A", 10), ("B", 20)]), "A: 10, B: 20")
16 | let parser = style.parseStrategy
17 | XCTAssertEqual(Dictionary(uniqueKeysWithValues: try parser.parse("A:10, B:20")), ["A": 10, "B": 20])
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.10
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SwiftFormats",
8 | platforms: [
9 | .iOS("16.0"),
10 | .macOS("13.0"),
11 | .macCatalyst("16.0"),
12 | ],
13 | products: [
14 | .library(
15 | name: "SwiftFormats",
16 | targets: ["SwiftFormats"]
17 | ),
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"),
21 | ],
22 | targets: [
23 | .target(
24 | name: "SwiftFormats",
25 | dependencies: [
26 | .product(name: "Algorithms", package: "swift-algorithms"),
27 | ]
28 | ),
29 | .testTarget(
30 | name: "SwiftFormatsTests",
31 | dependencies: ["SwiftFormats"]
32 | ),
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Support.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension TextEditor {
4 | init (value: Binding, format: Format) where Format: ParseableFormatStyle, Format.FormatInput == Value, Format.FormatOutput == String {
5 | var safe = true
6 | var string = format.format(value.wrappedValue)
7 |
8 | let binding = Binding {
9 | if safe {
10 | return format.format(value.wrappedValue)
11 | }
12 | else {
13 | return string
14 | }
15 | } set: { newValue in
16 | do {
17 | value.wrappedValue = try format.parseStrategy.parse(newValue)
18 | }
19 | catch {
20 | safe = false
21 | string = newValue
22 | }
23 | }
24 | self.init(text: binding)
25 | }
26 | }
27 |
28 | extension Text {
29 | init (value: Value, format: Format) where Format: FormatStyle, Format.FormatInput == Value, Format.FormatOutput == String {
30 | self.init(format.format(value))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Debugging.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct DescribedFormatStyle: FormatStyle {
4 | public typealias FormatInput = FormatInput
5 | public typealias FormatOutput = String
6 |
7 | public func format(_ value: FormatInput) -> String {
8 | String(describing: value)
9 | }
10 | }
11 |
12 | public extension FormatStyle where Self == DescribedFormatStyle {
13 | /// A format style that uses `String(describing:)` to format the value. Useful for debugging.
14 | ///
15 | /// Example:
16 | /// - `"\(123, format: .described)"`` vs `String(describing: 123)` // :shrug:
17 | static var described: DescribedFormatStyle {
18 | DescribedFormatStyle()
19 | }
20 | }
21 |
22 | // MARK: -
23 |
24 | public struct DumpedFormatStyle: FormatStyle {
25 | public typealias FormatInput = FormatInput
26 | public typealias FormatOutput = String
27 |
28 | public func format(_ value: FormatInput) -> String {
29 | var s = ""
30 | dump(value, to: &s)
31 | return s
32 | }
33 | }
34 |
35 | public extension FormatStyle where Self == DescribedFormatStyle {
36 | /// A format style that uses `dump` to format the value. Useful for debugging.
37 | static var dumped: DumpedFormatStyle {
38 | DumpedFormatStyle()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+JSON.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Format `Encodable`` types as JSON strings.
4 | /// - Example:
5 | /// - `TextField(text: "JSON", value: $value, format: JSONFormatStyle())`
6 | public struct JSONFormatStyle : FormatStyle where FormatInput: Encodable {
7 |
8 | public init() {
9 | }
10 |
11 | public func format(_ value: FormatInput) -> String {
12 | do {
13 | let data = try JSONEncoder().encode(value)
14 | return String(decoding: data, as: UTF8.self)
15 | }
16 | catch {
17 | fatalError("\(error)")
18 | }
19 | }
20 | }
21 |
22 | extension JSONFormatStyle: ParseableFormatStyle where FormatInput: Decodable {
23 | public var parseStrategy: JSONParseStrategy {
24 | return JSONParseStrategy()
25 | }
26 | }
27 |
28 | public struct JSONParseStrategy : ParseStrategy where ParseOutput: Decodable {
29 |
30 | public enum JSONParseError: Error {
31 | case couldNotDecodeData
32 | }
33 |
34 | public init() {
35 | }
36 |
37 | public func parse(_ value: String) throws -> ParseOutput {
38 | guard let data = value.data(using: .utf8) else {
39 | throw JSONParseError.couldNotDecodeData
40 | }
41 | return try JSONDecoder().decode(ParseOutput.self, from: data)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/Support.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import simd
3 | import RegexBuilder
4 |
5 | public enum SwiftFormatsError: Error {
6 | case parseError
7 | case unitCannotBeDetermined
8 | case missingKeys
9 | case countError
10 | }
11 |
12 | internal func unimplemented(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Never {
13 | fatalError(message(), file: file, line: line)
14 | }
15 |
16 | internal func degreesToRadians(_ value: F) -> F where F: FloatingPoint {
17 | value * .pi / 180
18 | }
19 |
20 | internal func radiansToDegrees(_ value: F) -> F where F: FloatingPoint {
21 | value * 180 / .pi
22 | }
23 |
24 | // MARK: -
25 |
26 | internal extension SIMD {
27 | var scalars: [Scalar] {
28 | (0 ..< scalarCount).map { self[$0] }
29 | }
30 | }
31 |
32 |
33 | /// Generates a ChoiceOf regex pattern from an array of strings.
34 | extension Array: RegexComponent where Element == String {
35 | public var regex: Regex {
36 |
37 | guard let first else {
38 | fatalError("Cannot create ChoiceOf with zero elements.")
39 | }
40 |
41 | return Regex {
42 | dropFirst().reduce(AlternationBuilder.buildPartialBlock(first: first)) { regex, element in
43 | return AlternationBuilder.buildPartialBlock(accumulated: regex, next: element)
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/SwiftFormatsTests/AngleTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftFormats
3 | import SwiftUI
4 | import XCTest
5 |
6 | class AngleValueTests: XCTestCase {
7 | func test1() {
8 | let angle = Angle(degrees: 90)
9 | XCTAssertEqual(angle.formatted(), "90°")
10 | XCTAssertEqual("\(angle, format: .angle)", "90°")
11 | XCTAssertEqual("\(angle, format: .angle.degrees)", "90°")
12 | XCTAssertEqual("\(angle, format: .angle.radians)", "1.570796rad")
13 |
14 | XCTAssertEqual(try AngleValueParseStrategy().parse("90°"), Angle(degrees: 90))
15 | XCTAssertEqual(try AngleValueParseStrategy(defaultInputUnit: .degrees).parse("90"), Angle(degrees: 90))
16 | XCTAssertEqual(try AngleValueParseStrategy().parse("1.570796rad").degrees, 90, accuracy: 0.001)
17 | XCTAssertEqual(try AngleValueParseStrategy().parse("90"), Angle(degrees: 90))
18 | XCTAssertThrowsError(try AngleValueParseStrategy(defaultInputUnit: nil).parse("90"))
19 | XCTAssertThrowsError(try AngleValueParseStrategy().parse("xxx°"))
20 |
21 | XCTAssertEqual(try AngleValueParseStrategy().parse(" 90°"), Angle(degrees: 90))
22 | XCTAssertEqual(try AngleValueParseStrategy().parse("90° "), Angle(degrees: 90))
23 | XCTAssertEqual(try AngleValueParseStrategy().parse("90 °"), Angle(degrees: 90))
24 | XCTAssertEqual(try AngleValueParseStrategy().parse(" 90 ° "), Angle(degrees: 90))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2023, Jonathan Wight
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/String+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension String {
4 | /// A convenience initializer for `String` that takes a `FormatStyle` and a value to format.
5 | ///
6 | /// - Discussion:
7 | /// Use as an alternative when the type you're formatting does not or cannot provide a `.formatted()` method.
8 | /// - Parameters:
9 | /// - input: The value to format.
10 | /// - format: The format style to use.
11 | /// - Example:
12 | /// - `String(123, format: .number)`
13 | init(_ input: F.FormatInput, format: F) where F: FormatStyle, F.FormatOutput == String {
14 | self = format.format(input)
15 | }
16 | }
17 |
18 | public extension String.StringInterpolation {
19 | /// Use format styles directly in string interpolation.
20 | /// - Example:
21 | /// - `"The value is \(123, format: .number)"`
22 | mutating func appendInterpolation(_ value: Value, format: Style) where Style: FormatStyle, Style.FormatOutput == String, Style.FormatInput == Value {
23 | appendInterpolation(format.format(value))
24 | }
25 | }
26 |
27 | public extension String.StringInterpolation {
28 | /// Format a `Measurement` directly in string interpolation.
29 | /// - Example:
30 | /// - `"The value is \(123, unit: .meters, format: .number) meters"`
31 | mutating func appendInterpolation