├── .github
└── workflows
│ └── swift.yml
├── .gitignore
├── .swift-version
├── .swiftformat
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── SwiftFormats.xcscheme
├── .vscode
└── settings.json
├── Demos
├── SwiftFormatsDemo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── SwiftFormatsDemo
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── Support.swift
│ ├── SwiftFormatsDemo.entitlements
│ └── SwiftFormatsDemoApp.swift
├── LICENSE.md
├── MyPlayground.playground
├── Contents.swift
└── contents.xcplayground
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
└── SwiftFormats
│ ├── BoolFormatStyle.swift
│ ├── DegreesMinutesSecondsNotation.swift
│ ├── FormatStyle+Angles.swift
│ ├── FormatStyle+ClosedRange.swift
│ ├── FormatStyle+Coordinates.swift
│ ├── FormatStyle+CoreGraphics.swift
│ ├── FormatStyle+Debugging.swift
│ ├── FormatStyle+Extensions.swift
│ ├── FormatStyle+Hexdump.swift
│ ├── FormatStyle+JSON.swift
│ ├── FormatStyle+Matrix.swift
│ ├── FormatStyle+Quaternion.swift
│ ├── FormatStyle+Vector.swift
│ ├── IncrementalParseStrategy.swift
│ ├── MappingFormatStyle.swift
│ ├── ParseableFormatStyle+Measurement.swift
│ ├── RadixedIntegerFormatStyle.swift
│ ├── Scratch.swift
│ ├── SimpleListFormatStyle.swift
│ ├── String+Extensions.swift
│ ├── Support.swift
│ └── TupleFormatStyle.swift
├── TestPlans
└── SwiftFormats.xctestplan
└── Tests
└── SwiftFormatsTests
├── AngleTests.swift
├── BoolTests.swift
├── CoreGraphicsTests.swift
├── MappingTests.swift
├── MatrixTests.swift
├── QuaternionTests.swift
├── SimpleListTests.swift
├── SwiftFormatsTests.swift
├── TupleTests.swift
└── VectorTests.swift
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.7
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | analyzer_rules:
2 | - capture_variable
3 | - explicit_self
4 | - typesafe_array_init
5 | - unused_declaration
6 | - unused_import
7 | only_rules:
8 | - accessibility_label_for_image
9 | - accessibility_trait_for_button
10 | - anonymous_argument_in_multiline_closure
11 | - anyobject_protocol
12 | - array_init
13 | - balanced_xctest_lifecycle
14 | - block_based_kvo
15 | - class_delegate_protocol
16 | - closing_brace
17 | - closure_body_length
18 | - closure_end_indentation
19 | - closure_parameter_position
20 | - closure_spacing
21 | - collection_alignment
22 | - colon
23 | - comma
24 | - comma_inheritance
25 | # - comment_spacing
26 | - compiler_protocol_init
27 | - computed_accessors_order
28 | - conditional_returns_on_newline
29 | - contains_over_filter_count
30 | - contains_over_filter_is_empty
31 | - contains_over_first_not_nil
32 | - contains_over_range_nil_comparison
33 | - control_statement
34 | - convenience_type
35 | - custom_rules
36 | - cyclomatic_complexity
37 | - deployment_target
38 | - discarded_notification_center_observer
39 | - discouraged_assert
40 | - discouraged_direct_init
41 | # - discouraged_none_name
42 | - discouraged_object_literal
43 | - discouraged_optional_boolean
44 | - discouraged_optional_collection
45 | - duplicate_enum_cases
46 | - duplicate_imports
47 | - duplicated_key_in_dictionary_literal
48 | - dynamic_inline
49 | - empty_collection_literal
50 | - empty_count
51 | - empty_enum_arguments
52 | - empty_parameters
53 | - empty_parentheses_with_trailing_closure
54 | - empty_string
55 | - empty_xctest_method
56 | - enum_case_associated_values_count
57 | - expiring_todo
58 | # - explicit_acl
59 | - explicit_enum_raw_value
60 | - explicit_init
61 | # - explicit_top_level_acl
62 | # - explicit_type_interface
63 | # - extension_access_modifier
64 | - fallthrough
65 | - fatal_error_message
66 | - file_header
67 | - file_length
68 | # - file_name
69 | # - file_name_no_space
70 | # - file_types_order
71 | - first_where
72 | - flatmap_over_map_reduce
73 | - for_where
74 | - force_cast
75 | - force_try
76 | # - force_unwrapping
77 | - function_body_length
78 | # - function_default_parameter_at_end
79 | - function_parameter_count
80 | - generic_type_name
81 | - ibinspectable_in_extension
82 | - identical_operands
83 | # - identifier_name
84 | - implicit_getter
85 | # - implicit_return
86 | - implicitly_unwrapped_optional
87 | - inclusive_language
88 | - indentation_width
89 | - inert_defer
90 | - is_disjoint
91 | - joined_default_parameter
92 | - large_tuple
93 | - last_where
94 | - leading_whitespace
95 | - legacy_cggeometry_functions
96 | - legacy_constant
97 | - legacy_constructor
98 | - legacy_hashing
99 | - legacy_multiple
100 | - legacy_nsgeometry_functions
101 | - legacy_objc_type
102 | - legacy_random
103 | # - let_var_whitespace
104 | # - line_length
105 | - literal_expression_end_indentation
106 | - local_doc_comment
107 | # - lower_acl_than_parent
108 | - mark
109 | # - missing_docs
110 | - modifier_order
111 | - multiline_arguments
112 | # - multiline_arguments_brackets
113 | - multiline_function_chains
114 | - multiline_literal_brackets
115 | - multiline_parameters
116 | - multiline_parameters_brackets
117 | - multiple_closures_with_trailing_closure
118 | - nesting
119 | - nimble_operator
120 | # - no_extension_access_modifier
121 | - no_fallthrough_only
122 | # - no_grouping_extension
123 | # - no_magic_numbers
124 | - no_space_in_method_call
125 | - notification_center_detachment
126 | - ns_number_init_as_function_reference
127 | - nslocalizedstring_key
128 | - nslocalizedstring_require_bundle
129 | - nsobject_prefer_isequal
130 | # - number_separator
131 | - object_literal
132 | - opening_brace
133 | - operator_usage_whitespace
134 | - operator_whitespace
135 | - optional_enum_case_matching
136 | - orphaned_doc_comment
137 | - overridden_super_call
138 | - override_in_extension
139 | - pattern_matching_keywords
140 | # - prefer_nimble
141 | - prefer_self_in_static_references
142 | - prefer_self_type_over_type_of_self
143 | - prefer_zero_over_explicit_init
144 | # - prefixed_toplevel_constant
145 | - private_action
146 | - private_outlet
147 | - private_over_fileprivate
148 | - private_subject
149 | - private_unit_test
150 | - prohibited_interface_builder
151 | - prohibited_super_call
152 | - protocol_property_accessors_order
153 | - quick_discouraged_call
154 | - quick_discouraged_focused_test
155 | - quick_discouraged_pending_test
156 | - raw_value_for_camel_cased_codable_enum
157 | - reduce_boolean
158 | - reduce_into
159 | - redundant_discardable_let
160 | - redundant_nil_coalescing
161 | - redundant_objc_attribute
162 | - redundant_optional_initialization
163 | - redundant_set_access_control
164 | - redundant_string_enum_value
165 | - redundant_type_annotation
166 | - redundant_void_return
167 | # - required_deinit
168 | - required_enum_case
169 | - return_arrow_whitespace
170 | - return_value_from_void_function
171 | - self_binding
172 | - self_in_property_initialization
173 | - shorthand_operator
174 | - shorthand_optional_binding
175 | # - single_test_class
176 | - sorted_first_last
177 | # - sorted_imports
178 | # - statement_position
179 | - static_operator
180 | - strict_fileprivate
181 | - strong_iboutlet
182 | # - superfluous_disable_command
183 | - switch_case_alignment
184 | - switch_case_on_newline
185 | - syntactic_sugar
186 | - test_case_accessibility
187 | # - todo
188 | - toggle_bool
189 | - trailing_closure
190 | # - trailing_comma
191 | # - trailing_newline
192 | - trailing_semicolon
193 | - trailing_whitespace
194 | - type_body_length
195 | # - type_contents_order
196 | - type_name
197 | - unavailable_condition
198 | - unavailable_function
199 | - unneeded_break_in_switch
200 | - unneeded_parentheses_in_closure_argument
201 | - unowned_variable_capture
202 | - untyped_error_in_catch
203 | - unused_capture_list
204 | - unused_closure_parameter
205 | - unused_control_flow_label
206 | - unused_enumerated
207 | - unused_optional_binding
208 | - unused_setter_value
209 | - valid_ibinspectable
210 | - vertical_parameter_alignment
211 | - vertical_parameter_alignment_on_call
212 | # - vertical_whitespace
213 | # - vertical_whitespace_between_cases
214 | # - vertical_whitespace_closing_braces
215 | # - vertical_whitespace_opening_braces
216 | - void_function_in_ternary
217 | - void_return
218 | - weak_delegate
219 | - xct_specific_matcher
220 | - xctfail_message
221 | - yoda_condition
222 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/SwiftFormats.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
34 |
35 |
36 |
37 |
39 |
45 |
46 |
47 |
48 |
49 |
59 |
60 |
66 |
67 |
73 |
74 |
75 |
76 |
78 |
79 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Parseable",
4 | "Substrategy"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 454C1ACB29ABC96700B77553 /* SwiftFormatsDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C1ACA29ABC96700B77553 /* SwiftFormatsDemoApp.swift */; };
11 | 454C1ACF29ABC96800B77553 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 454C1ACE29ABC96800B77553 /* Assets.xcassets */; };
12 | 454C1AD329ABC96800B77553 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 454C1AD229ABC96800B77553 /* Preview Assets.xcassets */; };
13 | 454C1ADD29ABC9EF00B77553 /* SwiftFormats in Frameworks */ = {isa = PBXBuildFile; productRef = 454C1ADC29ABC9EF00B77553 /* SwiftFormats */; };
14 | 45D31CB029F1A33400EF9317 /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D31CAF29F1A33400EF9317 /* Support.swift */; };
15 | 45F39EB329F06A6600715361 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F39EB229F06A6600715361 /* ContentView.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | 454C1AC729ABC96700B77553 /* SwiftFormatsDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftFormatsDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 454C1ACA29ABC96700B77553 /* SwiftFormatsDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftFormatsDemoApp.swift; sourceTree = ""; };
21 | 454C1ACE29ABC96800B77553 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
22 | 454C1AD029ABC96800B77553 /* SwiftFormatsDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftFormatsDemo.entitlements; sourceTree = ""; };
23 | 454C1AD229ABC96800B77553 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | 45D31CAF29F1A33400EF9317 /* Support.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Support.swift; sourceTree = ""; };
25 | 45F39EB129F06A0700715361 /* SwiftFormats */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftFormats; path = ..; sourceTree = ""; };
26 | 45F39EB229F06A6600715361 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | 454C1AC429ABC96700B77553 /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | 454C1ADD29ABC9EF00B77553 /* SwiftFormats in Frameworks */,
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXFrameworksBuildPhase section */
39 |
40 | /* Begin PBXGroup section */
41 | 454C1ABE29ABC96700B77553 = {
42 | isa = PBXGroup;
43 | children = (
44 | 454C1AC929ABC96700B77553 /* SwiftFormatsDemo */,
45 | 454C1AD929ABC99000B77553 /* Packages */,
46 | 454C1AC829ABC96700B77553 /* Products */,
47 | 454C1ADB29ABC9EF00B77553 /* Frameworks */,
48 | );
49 | sourceTree = "";
50 | };
51 | 454C1AC829ABC96700B77553 /* Products */ = {
52 | isa = PBXGroup;
53 | children = (
54 | 454C1AC729ABC96700B77553 /* SwiftFormatsDemo.app */,
55 | );
56 | name = Products;
57 | sourceTree = "";
58 | };
59 | 454C1AC929ABC96700B77553 /* SwiftFormatsDemo */ = {
60 | isa = PBXGroup;
61 | children = (
62 | 454C1ACA29ABC96700B77553 /* SwiftFormatsDemoApp.swift */,
63 | 45F39EB229F06A6600715361 /* ContentView.swift */,
64 | 45D31CAF29F1A33400EF9317 /* Support.swift */,
65 | 454C1ACE29ABC96800B77553 /* Assets.xcassets */,
66 | 454C1AD029ABC96800B77553 /* SwiftFormatsDemo.entitlements */,
67 | 454C1AD129ABC96800B77553 /* Preview Content */,
68 | );
69 | path = SwiftFormatsDemo;
70 | sourceTree = "";
71 | };
72 | 454C1AD129ABC96800B77553 /* Preview Content */ = {
73 | isa = PBXGroup;
74 | children = (
75 | 454C1AD229ABC96800B77553 /* Preview Assets.xcassets */,
76 | );
77 | path = "Preview Content";
78 | sourceTree = "";
79 | };
80 | 454C1AD929ABC99000B77553 /* Packages */ = {
81 | isa = PBXGroup;
82 | children = (
83 | 45F39EB129F06A0700715361 /* SwiftFormats */,
84 | );
85 | name = Packages;
86 | sourceTree = "";
87 | };
88 | 454C1ADB29ABC9EF00B77553 /* Frameworks */ = {
89 | isa = PBXGroup;
90 | children = (
91 | );
92 | name = Frameworks;
93 | sourceTree = "";
94 | };
95 | /* End PBXGroup section */
96 |
97 | /* Begin PBXNativeTarget section */
98 | 454C1AC629ABC96700B77553 /* SwiftFormatsDemo */ = {
99 | isa = PBXNativeTarget;
100 | buildConfigurationList = 454C1AD629ABC96800B77553 /* Build configuration list for PBXNativeTarget "SwiftFormatsDemo" */;
101 | buildPhases = (
102 | 454C1AC329ABC96700B77553 /* Sources */,
103 | 454C1AC429ABC96700B77553 /* Frameworks */,
104 | 454C1AC529ABC96700B77553 /* Resources */,
105 | );
106 | buildRules = (
107 | );
108 | dependencies = (
109 | );
110 | name = SwiftFormatsDemo;
111 | packageProductDependencies = (
112 | 454C1ADC29ABC9EF00B77553 /* SwiftFormats */,
113 | );
114 | productName = SwiftFormatsDemo;
115 | productReference = 454C1AC729ABC96700B77553 /* SwiftFormatsDemo.app */;
116 | productType = "com.apple.product-type.application";
117 | };
118 | /* End PBXNativeTarget section */
119 |
120 | /* Begin PBXProject section */
121 | 454C1ABF29ABC96700B77553 /* Project object */ = {
122 | isa = PBXProject;
123 | attributes = {
124 | BuildIndependentTargetsInParallel = 1;
125 | LastSwiftUpdateCheck = 1430;
126 | LastUpgradeCheck = 1430;
127 | TargetAttributes = {
128 | 454C1AC629ABC96700B77553 = {
129 | CreatedOnToolsVersion = 14.3;
130 | };
131 | };
132 | };
133 | buildConfigurationList = 454C1AC229ABC96700B77553 /* Build configuration list for PBXProject "SwiftFormatsDemo" */;
134 | compatibilityVersion = "Xcode 14.0";
135 | developmentRegion = en;
136 | hasScannedForEncodings = 0;
137 | knownRegions = (
138 | en,
139 | Base,
140 | );
141 | mainGroup = 454C1ABE29ABC96700B77553;
142 | productRefGroup = 454C1AC829ABC96700B77553 /* Products */;
143 | projectDirPath = "";
144 | projectRoot = "";
145 | targets = (
146 | 454C1AC629ABC96700B77553 /* SwiftFormatsDemo */,
147 | );
148 | };
149 | /* End PBXProject section */
150 |
151 | /* Begin PBXResourcesBuildPhase section */
152 | 454C1AC529ABC96700B77553 /* Resources */ = {
153 | isa = PBXResourcesBuildPhase;
154 | buildActionMask = 2147483647;
155 | files = (
156 | 454C1AD329ABC96800B77553 /* Preview Assets.xcassets in Resources */,
157 | 454C1ACF29ABC96800B77553 /* Assets.xcassets in Resources */,
158 | );
159 | runOnlyForDeploymentPostprocessing = 0;
160 | };
161 | /* End PBXResourcesBuildPhase section */
162 |
163 | /* Begin PBXSourcesBuildPhase section */
164 | 454C1AC329ABC96700B77553 /* Sources */ = {
165 | isa = PBXSourcesBuildPhase;
166 | buildActionMask = 2147483647;
167 | files = (
168 | 45D31CB029F1A33400EF9317 /* Support.swift in Sources */,
169 | 454C1ACB29ABC96700B77553 /* SwiftFormatsDemoApp.swift in Sources */,
170 | 45F39EB329F06A6600715361 /* ContentView.swift in Sources */,
171 | );
172 | runOnlyForDeploymentPostprocessing = 0;
173 | };
174 | /* End PBXSourcesBuildPhase section */
175 |
176 | /* Begin XCBuildConfiguration section */
177 | 454C1AD429ABC96800B77553 /* Debug */ = {
178 | isa = XCBuildConfiguration;
179 | buildSettings = {
180 | ALWAYS_SEARCH_USER_PATHS = NO;
181 | CLANG_ANALYZER_NONNULL = YES;
182 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
184 | CLANG_ENABLE_MODULES = YES;
185 | CLANG_ENABLE_OBJC_ARC = YES;
186 | CLANG_ENABLE_OBJC_WEAK = YES;
187 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
188 | CLANG_WARN_BOOL_CONVERSION = YES;
189 | CLANG_WARN_COMMA = YES;
190 | CLANG_WARN_CONSTANT_CONVERSION = YES;
191 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
193 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
194 | CLANG_WARN_EMPTY_BODY = YES;
195 | CLANG_WARN_ENUM_CONVERSION = YES;
196 | CLANG_WARN_INFINITE_RECURSION = YES;
197 | CLANG_WARN_INT_CONVERSION = YES;
198 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
199 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
200 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
201 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
202 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
204 | CLANG_WARN_STRICT_PROTOTYPES = YES;
205 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
206 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
207 | CLANG_WARN_UNREACHABLE_CODE = YES;
208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
209 | COPY_PHASE_STRIP = NO;
210 | DEBUG_INFORMATION_FORMAT = dwarf;
211 | ENABLE_STRICT_OBJC_MSGSEND = YES;
212 | ENABLE_TESTABILITY = YES;
213 | GCC_C_LANGUAGE_STANDARD = gnu11;
214 | GCC_DYNAMIC_NO_PIC = NO;
215 | GCC_NO_COMMON_BLOCKS = YES;
216 | GCC_OPTIMIZATION_LEVEL = 0;
217 | GCC_PREPROCESSOR_DEFINITIONS = (
218 | "DEBUG=1",
219 | "$(inherited)",
220 | );
221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
223 | GCC_WARN_UNDECLARED_SELECTOR = YES;
224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
225 | GCC_WARN_UNUSED_FUNCTION = YES;
226 | GCC_WARN_UNUSED_VARIABLE = YES;
227 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
228 | MTL_FAST_MATH = YES;
229 | ONLY_ACTIVE_ARCH = YES;
230 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
231 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
232 | };
233 | name = Debug;
234 | };
235 | 454C1AD529ABC96800B77553 /* Release */ = {
236 | isa = XCBuildConfiguration;
237 | buildSettings = {
238 | ALWAYS_SEARCH_USER_PATHS = NO;
239 | CLANG_ANALYZER_NONNULL = YES;
240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
242 | CLANG_ENABLE_MODULES = YES;
243 | CLANG_ENABLE_OBJC_ARC = YES;
244 | CLANG_ENABLE_OBJC_WEAK = YES;
245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
246 | CLANG_WARN_BOOL_CONVERSION = YES;
247 | CLANG_WARN_COMMA = YES;
248 | CLANG_WARN_CONSTANT_CONVERSION = YES;
249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
252 | CLANG_WARN_EMPTY_BODY = YES;
253 | CLANG_WARN_ENUM_CONVERSION = YES;
254 | CLANG_WARN_INFINITE_RECURSION = YES;
255 | CLANG_WARN_INT_CONVERSION = YES;
256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
262 | CLANG_WARN_STRICT_PROTOTYPES = YES;
263 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
265 | CLANG_WARN_UNREACHABLE_CODE = YES;
266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
267 | COPY_PHASE_STRIP = NO;
268 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
269 | ENABLE_NS_ASSERTIONS = NO;
270 | ENABLE_STRICT_OBJC_MSGSEND = YES;
271 | GCC_C_LANGUAGE_STANDARD = gnu11;
272 | GCC_NO_COMMON_BLOCKS = YES;
273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
275 | GCC_WARN_UNDECLARED_SELECTOR = YES;
276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
277 | GCC_WARN_UNUSED_FUNCTION = YES;
278 | GCC_WARN_UNUSED_VARIABLE = YES;
279 | MTL_ENABLE_DEBUG_INFO = NO;
280 | MTL_FAST_MATH = YES;
281 | SWIFT_COMPILATION_MODE = wholemodule;
282 | SWIFT_OPTIMIZATION_LEVEL = "-O";
283 | };
284 | name = Release;
285 | };
286 | 454C1AD729ABC96800B77553 /* Debug */ = {
287 | isa = XCBuildConfiguration;
288 | buildSettings = {
289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
291 | CODE_SIGN_ENTITLEMENTS = SwiftFormatsDemo/SwiftFormatsDemo.entitlements;
292 | CODE_SIGN_STYLE = Automatic;
293 | CURRENT_PROJECT_VERSION = 1;
294 | DEVELOPMENT_ASSET_PATHS = ".. SwiftFormatsDemo/Preview\\ Content";
295 | DEVELOPMENT_TEAM = 6E23EP94PG;
296 | ENABLE_HARDENED_RUNTIME = YES;
297 | ENABLE_PREVIEWS = YES;
298 | GENERATE_INFOPLIST_FILE = YES;
299 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
300 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
301 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
302 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
303 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
304 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
305 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
306 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
307 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
308 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
309 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
310 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
311 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
312 | MACOSX_DEPLOYMENT_TARGET = 13.2;
313 | MARKETING_VERSION = 1.0;
314 | PRODUCT_BUNDLE_IDENTIFIER = io.schwa.SwiftFormatsDemo;
315 | PRODUCT_NAME = "$(TARGET_NAME)";
316 | SDKROOT = auto;
317 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
318 | SWIFT_EMIT_LOC_STRINGS = YES;
319 | SWIFT_VERSION = 5.0;
320 | TARGETED_DEVICE_FAMILY = "1,2";
321 | };
322 | name = Debug;
323 | };
324 | 454C1AD829ABC96800B77553 /* Release */ = {
325 | isa = XCBuildConfiguration;
326 | buildSettings = {
327 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
328 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
329 | CODE_SIGN_ENTITLEMENTS = SwiftFormatsDemo/SwiftFormatsDemo.entitlements;
330 | CODE_SIGN_STYLE = Automatic;
331 | CURRENT_PROJECT_VERSION = 1;
332 | DEVELOPMENT_ASSET_PATHS = ".. SwiftFormatsDemo/Preview\\ Content";
333 | DEVELOPMENT_TEAM = 6E23EP94PG;
334 | ENABLE_HARDENED_RUNTIME = YES;
335 | ENABLE_PREVIEWS = YES;
336 | GENERATE_INFOPLIST_FILE = YES;
337 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
338 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
339 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
340 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
341 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
342 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
343 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
344 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
345 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
346 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
347 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
348 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
349 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
350 | MACOSX_DEPLOYMENT_TARGET = 13.2;
351 | MARKETING_VERSION = 1.0;
352 | PRODUCT_BUNDLE_IDENTIFIER = io.schwa.SwiftFormatsDemo;
353 | PRODUCT_NAME = "$(TARGET_NAME)";
354 | SDKROOT = auto;
355 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
356 | SWIFT_EMIT_LOC_STRINGS = YES;
357 | SWIFT_VERSION = 5.0;
358 | TARGETED_DEVICE_FAMILY = "1,2";
359 | };
360 | name = Release;
361 | };
362 | /* End XCBuildConfiguration section */
363 |
364 | /* Begin XCConfigurationList section */
365 | 454C1AC229ABC96700B77553 /* Build configuration list for PBXProject "SwiftFormatsDemo" */ = {
366 | isa = XCConfigurationList;
367 | buildConfigurations = (
368 | 454C1AD429ABC96800B77553 /* Debug */,
369 | 454C1AD529ABC96800B77553 /* Release */,
370 | );
371 | defaultConfigurationIsVisible = 0;
372 | defaultConfigurationName = Release;
373 | };
374 | 454C1AD629ABC96800B77553 /* Build configuration list for PBXNativeTarget "SwiftFormatsDemo" */ = {
375 | isa = XCConfigurationList;
376 | buildConfigurations = (
377 | 454C1AD729ABC96800B77553 /* Debug */,
378 | 454C1AD829ABC96800B77553 /* Release */,
379 | );
380 | defaultConfigurationIsVisible = 0;
381 | defaultConfigurationName = Release;
382 | };
383 | /* End XCConfigurationList section */
384 |
385 | /* Begin XCSwiftPackageProductDependency section */
386 | 454C1ADC29ABC9EF00B77553 /* SwiftFormats */ = {
387 | isa = XCSwiftPackageProductDependency;
388 | productName = SwiftFormats;
389 | };
390 | /* End XCSwiftPackageProductDependency section */
391 | };
392 | rootObject = 454C1ABF29ABC96700B77553 /* Project object */;
393 | }
394 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftFormats
3 | import simd
4 | import CoreLocation
5 |
6 | protocol DefaultInitialisable {
7 | init()
8 | }
9 |
10 | struct ContentView: View {
11 | var body: some View {
12 | NavigationView {
13 | List {
14 | demo(of: AngleEditorDemoView.self)
15 | demo(of: ClosedRangeEditorDemoView.self)
16 | demo(of: CoordinatesEditorDemoView.self)
17 | demo(of: HexDumpFormatDemoView.self)
18 | demo(of: JSONFormatDemoView.self)
19 | demo(of: MatrixEditorDemoView.self)
20 | demo(of: PointEditorDemoView.self)
21 | demo(of: QuaternionDemoView.self)
22 | demo(of: VectorEditorDemoView.self)
23 | demo(of: AngleRangeEditorDemo.self)
24 | }
25 | }
26 | }
27 |
28 | func demo(of t: T.Type) -> some View where T: View & DefaultInitialisable {
29 | let name = String(String(describing: type(of: t)).prefix(while: { $0 != "." }))
30 | return NavigationLink(name) {
31 | t.init()
32 | }
33 | }
34 | }
35 |
36 | // MARK: -
37 |
38 | struct AngleEditorDemoView: View, DefaultInitialisable {
39 | @State
40 | var value: Double = 45
41 |
42 | @State
43 | var angleValue = Angle.degrees(45)
44 |
45 | var body: some View {
46 | Form {
47 | Section("String(describing:)") {
48 | Text(verbatim: "\(value)")
49 | }
50 | Section("Formatting TextField (degrees)") {
51 | TextField("Degrees", value: $value, format: .angle(inputUnit: .degrees, outputUnit: .degrees))
52 | .labelsHidden()
53 | .frame(maxWidth: 160)
54 | }
55 | Section("Formatting TextField (radians)") {
56 | TextField("Radians", value: $value, format: .angle(inputUnit: .degrees, outputUnit: .radians))
57 | .labelsHidden()
58 | .frame(maxWidth: 160)
59 | }
60 |
61 | Section("String(describing:)") {
62 | Text(verbatim: "\(angleValue)")
63 | }
64 | Section("Formatting TextField (degrees)") {
65 | TextField("Degrees", value: $angleValue, format: .angle.defaultInputUnit(.degrees))
66 | .labelsHidden()
67 | .frame(maxWidth: 160)
68 | }
69 | Section("Formatting TextField (radians)") {
70 | TextField("Radians", value: $angleValue, format: .angle)
71 | .labelsHidden()
72 | .frame(maxWidth: 160)
73 | }
74 | }
75 | }
76 | }
77 |
78 | // MARK: -
79 |
80 | struct ClosedRangeEditorDemoView: View, DefaultInitialisable {
81 | @State
82 | var value = 1...10
83 |
84 | var body: some View {
85 | Form {
86 | Section("String(describing:)") {
87 | Text(verbatim: "\(value)")
88 | }
89 | Section("Formatting TextField") {
90 | TextField("Value", value: $value, format: ClosedRangeFormatStyle(substyle: .number))
91 | .labelsHidden()
92 | .frame(maxWidth: 160)
93 | }
94 | }
95 | }
96 | }
97 |
98 | // MARK: -
99 |
100 | struct CoordinatesEditorDemoView: View, DefaultInitialisable {
101 | @State
102 | var value = CLLocationCoordinate2D(latitude: 45, longitude: 45)
103 |
104 | var body: some View {
105 | Text("Broken!")
106 | // Form {
107 | // Section("String(describing:)") {
108 | // Text(verbatim: "\(value)")
109 | // }
110 | // Section("Formatted Text") {
111 | // Text("\(value, format: .coordinates)")
112 | // }
113 | // }
114 | }
115 | }
116 |
117 | struct HexDumpFormatDemoView: View, DefaultInitialisable {
118 | @State
119 | var value: Data = "Hello world".data(using: .utf8)!
120 |
121 | var body: some View {
122 | Form {
123 | Section("String(describing:)") {
124 | Text(verbatim: "\(value)")
125 | }
126 | Section("Formatted Text") {
127 | Text("\(value, format: .hexdump())")
128 | .font(.body.monospaced())
129 | }
130 | }
131 | }
132 | }
133 |
134 | struct JSONFormatDemoView: View, DefaultInitialisable {
135 | @State
136 | var value = ["Hello": "World"]
137 |
138 | var body: some View {
139 | Form {
140 | Section("String(describing:)") {
141 | Text(verbatim: "\(value)")
142 | }
143 | Section("Formatting TextField") {
144 | TextField("json", value: $value, format: JSONFormatStyle())
145 | }
146 | }
147 | }
148 | }
149 |
150 | // MARK: -
151 |
152 | struct QuaternionDemoView: View, DefaultInitialisable {
153 | @State
154 | var value = simd_quatd(real: 0, imag: [0, 0, 0])
155 |
156 | var body: some View {
157 | Form {
158 | Section("String(describing:)") {
159 | Text(verbatim: "\(value)")
160 | }
161 | Section("Formatted Text") {
162 | Text(value: value, format: .quaternion)
163 | }
164 | Section("Formatting TextField") {
165 | TextField("value", value: $value, format: .quaternion)
166 | }
167 | }
168 | }
169 | }
170 |
171 | // MARK: -
172 |
173 | struct MatrixEditorDemoView: View, DefaultInitialisable {
174 | @State
175 | var value = simd_float4x4()
176 |
177 | var body: some View {
178 | Form {
179 | Section("String(describing:)") {
180 | Text(verbatim: "\(value)")
181 | }
182 | Section("Formatted Text") {
183 | Text(value, format: .matrix)
184 | }
185 | Section("Formatting TextField") {
186 | TextField("matrix", value: $value, format: .matrix)
187 | .lineLimit(4, reservesSpace: true)
188 | .labelsHidden()
189 | .frame(maxWidth: 160)
190 | }
191 | Section("Formatting TextEditor") {
192 | TextEditor(value: $value, format: .matrix)
193 | .lineLimit(4, reservesSpace: true)
194 | .frame(maxHeight: 200)
195 | }
196 | }
197 | }
198 | }
199 |
200 | // MARK: -
201 |
202 | struct PointEditorDemoView: View, DefaultInitialisable {
203 | @State
204 | var value = CGPoint.zero
205 |
206 | var body: some View {
207 | Form {
208 | Section("String(describing:)") {
209 | Text(verbatim: "\(value)")
210 | }
211 | Section("Formatting TextField") {
212 | TextField("Value", value: $value, format: .point)
213 | .labelsHidden()
214 | .frame(maxWidth: 160)
215 | }
216 | }
217 | }
218 | }
219 |
220 | // MARK: -
221 |
222 | struct VectorEditorDemoView: View, DefaultInitialisable {
223 |
224 | @State
225 | var value: SIMD3 = [0, 0, 0]
226 |
227 | var body: some View {
228 | Form {
229 | Text(verbatim: "\(value)")
230 | Section("Value") {
231 | Text("\(value, format: .vector)")
232 | }
233 | Section("Mapping Style") {
234 | TextField("Vector", value: $value, format: .vector)
235 | }
236 | Section("List Style") {
237 | TextField("Vector", value: $value, format: .vector.compositeStyle(.list))
238 | }
239 | }
240 | }
241 | }
242 |
243 | // MARK: -
244 |
245 | struct AngleRangeEditorDemo: View, DefaultInitialisable {
246 |
247 | @State
248 | var value: ClosedRange = .degrees(0) ... .degrees(180)
249 |
250 | var body: some View {
251 | TextField("Angles", value: $value, format: ClosedRangeFormatStyle(substyle: .angle))
252 | }
253 |
254 |
255 | }
256 |
--------------------------------------------------------------------------------
/Demos/SwiftFormatsDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/MyPlayground.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftFormats
3 | import simd
4 | import PlaygroundSupport
5 | import SwiftUI
6 |
7 |
8 | //Locale.availableIdentifiers.map { Locale(identifier: $0) }.forEach { locale in
9 | //
10 | // print(locale, terminator: "\t")
11 | //
12 | // let measurements: [Measurement] = [
13 | // Measurement(value: 45.123, unit: UnitAngle.degrees),
14 | // Measurement(value: 45.123, unit: UnitAngle.arcMinutes),
15 | // Measurement(value: 45.123, unit: UnitAngle.arcSeconds),
16 | // Measurement(value: 45.123, unit: UnitAngle.radians),
17 | // Measurement(value: 45.123, unit: UnitAngle.gradians),
18 | // Measurement(value: 45.123, unit: UnitAngle.revolutions),
19 | // ]
20 | //
21 | // for measurement in measurements {
22 | // print(measurement.formatted(.measurement(width: .abbreviated).locale(locale)), terminator: "\t")
23 | // print(measurement.formatted(.measurement(width: .narrow).locale(locale)), terminator: "\t")
24 | // print(measurement.formatted(.measurement(width: .wide).locale(locale)), terminator: "\t")
25 | // }
26 | // print("")
27 | //}
28 |
29 | //let f = MeasurementFormatter()
30 | //f.unitStyle = .long
31 | //f.unitOptions = .providedUnit
32 | //f
33 | //f.getObjectValue(nil, for: "45°", errorDescription: nil)
34 | ////struct ContentView: View {
35 | ////
36 | //// @State
37 | //// var value = 1.0 ... 2.0
38 | ////
39 | //// var body: some View {
40 | //// VStack {
41 | //// TextField("Value", value: $value, format: ClosedRangeFormatStyle(substyle: .number))
42 | //// Text(verbatim: "\(value)")
43 | //// }
44 | //// .frame(width: 320, height: 240)
45 | //// .border(Color.red)
46 | //// }
47 | ////}
48 | ////
49 | ////
50 | ////PlaygroundPage.current.setLiveView(ContentView())
51 |
--------------------------------------------------------------------------------
/MyPlayground.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftFormats
2 |
3 | More `FormatStyle` implementation for more types.
4 |
5 | ## Description
6 |
7 | This package provides more `FormatStyle` implementations for various types. Many types also provide `parserStrategy` implementations for parsing strings into the type where appropriate.
8 |
9 | It also provides extensions for String and string interpolation to make it easier to format values.
10 |
11 | ## Types
12 |
13 |
14 |
15 | | Name | In (1) | Out (2) | Format (3) | Parser (4) | Accessor (5) | Notes |
16 | |------|--------------------------|----------|-----------------------|------------|---------------|--------------------------------------|
17 | | | `BinaryFloatingPoint` | `String` | Angles | Yes | `angle` | Radians, degrees, etc |
18 | | | `BinaryFloatingPoint` | `String` | Degree Minute Seconds | No | `dmsNotation` | |
19 | | | `CGPoint` | `String` | List (6) | Yes | `point` | |
20 | | | `ClosedRange` | `String` | `X ... Y` | Yes | No | |
21 | | | `CLLocationCoordinate2D` | `String` | List | No | `coordinates` | |
22 | | | `BinaryFloatingPoint` | `String` | Latitude | No | `latitude` | Including hemisphere |
23 | | | `BinaryFloatingPoint` | `String` | Longitude | No | `longitude` | Including hemisphere |
24 | | | `Any` | `String` | Description | No | `describing` | Uses `String(describing:)` |
25 | | | `Any` | `String` | Dump | No | `dumped` | Uses `dump()` |
26 | | | `DataProtocol` | `String` | Hex-dumped | No | `hexdumped` | |
27 | | | `Codable` | `String` | JSON | Yes | `json` | Uses `JSONEncoder` and `JSONDecoder` |
28 | | | `SIMD3` | `String` | List or mapping | Yes | `vector` | |
29 | | | SIMD matrix types | `String` | List | Yes | `matrix` | |
30 | | | `BinaryInteger` | `String` | Radixed format | No | Various | Binary, Octal, Hex representations |
31 |
32 |
33 | ### Notes
34 |
35 | 1: Type provided as `FormatInput` in the `FormatStyle` implementation.
36 | 2: Type provided as `FormatOutput` in the `FormatStyle` implementation.
37 | 3: Format of the output.
38 | 4: Whether the `FormatStyle` implementation provides a corresponding `ParserStrategy`.
39 | 5: Whether a convenience property is provided to access style on `FormatStyle`.
40 | 6: Formats the input as a comma-separated list.
41 |
42 | ## Examples
43 |
44 | ### String interpolation
45 |
46 | ```swift
47 | let number = 123.456
48 | let formatted = "The number is \(number, .number)"
49 | // formatted == "The number is 123.456"
50 | ```
51 |
52 | ### CoreGraphics
53 |
54 | ```swift
55 | let point = CGPoint(x: 1.234, y: 5.678)
56 | let formatted = point.formatted(.decimal(places: 2))
57 | // formatted == "(1.23, 5.68)"
58 | ```
59 |
60 | ## Future Plans
61 |
62 | The initial priority is to expose formats and parsers for more SIMD/CG types. Some common helper format styles will be added (e.g. "field" parser - see TODO).
63 |
64 | ### TODOs
65 |
66 | - [ ] Find and handle all the TODOs
67 | - [ ] Clean up the parser init methods. Foundation parsers do not have public .init() methods and are only created via corresponding FormatStyles. Follow this. This will be API breaking.
68 | - [ ] Add more `.formatted()` and `.formatted(_ style:)` functions where appropriate
69 | - [ ] Track this in tabel
70 | - [ ] Add sugar for parsing (does a standard library equivalent to `.formatted()` exist?)
71 | - [ ] Investigate attribute strings and other non-string `FormatOutput` types
72 | - [ ] More CoreGraphics types
73 | - [ ] Yet another CGColor to web colour converter
74 | - [ ] Do all SIMD types in a sane way
75 | - [ ] Make a "field" type generic format, e.g. represent CGPoint as `x: 1.234, y: 5.678` (use for SIMD and other CG types)
76 | - [ ] A parser for angle would be nice but `Measurement` has no parser we can base it off.
77 | - [ ] Investigate a "Parsable" and "Formattable" protocol that provides a .formatted() etc functions.
78 | - [ ] Add support for SwiftUI.Angle - how do we differentiate between two different Angle formatters?
79 | - [ ] Add support for Spatial.framework types
80 | - [ ] Make quaternion and vector parsers more accepting of styles.
81 | - [ ] Make quaternion parser treat angles as angle foramts (i.e. with °)
82 | - [X] Make angle parser not care and parse rad or ° correctly.
83 | - [ ] Test 'radian' vs 'radians' vs localized.
84 | - [ ] In general parsers need to be configured less and accept more formats.
85 | - [ ] Real docc documentation.
86 | - [ ] Fuzz parsers and see what fun will ensue.
87 |
88 | ## Resources
89 |
90 | ### Apple
91 |
92 | -
93 | -
94 | -
95 |
96 | ### Other
97 |
98 | -
99 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/BoolFormatStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // TODO: Localisation?
4 |
5 | public struct BoolFormatStyle: FormatStyle {
6 |
7 | var falseString: String
8 | var trueString: String
9 |
10 | public init(_ falseString: String = "false", _ trueString: String = "true") {
11 | self.falseString = falseString
12 | self.trueString = trueString
13 | }
14 |
15 | public func format(_ value: Bool) -> String {
16 | switch value {
17 | case true:
18 | return trueString
19 | case false:
20 | return falseString
21 | }
22 | }
23 | }
24 |
25 | public extension BoolFormatStyle {
26 |
27 | func values(_ falseString: String = "false", _ trueString: String = "true") -> Self {
28 | return self.false(falseString).true(trueString)
29 | }
30 |
31 | func `true`(_ string: String) -> Self {
32 | var copy = self
33 | copy.trueString = string
34 | return copy
35 | }
36 |
37 | func `false`(_ string: String) -> Self {
38 | var copy = self
39 | copy.falseString = string
40 | return copy
41 | }
42 | }
43 |
44 | public extension FormatStyle where Self == BoolFormatStyle {
45 | static var bool: BoolFormatStyle {
46 | return BoolFormatStyle()
47 | }
48 | }
49 |
50 | public extension Bool {
51 | func formatted() -> String {
52 | return formatted(.bool)
53 | }
54 |
55 | func formatted(_ style: S) -> S.FormatOutput where S: FormatStyle, S.FormatInput == Bool {
56 | return style.format(self)
57 | }
58 | }
59 |
60 | // MARK: -
61 |
62 | public struct BoolParseStrategy: ParseStrategy {
63 |
64 | public init() {
65 | }
66 |
67 | // This is really quick and simple.
68 | public func parse(_ value: String) throws -> Bool {
69 | let value = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
70 | switch value {
71 | case "true", "yes", "1":
72 | return true
73 | case "false", "no", "0":
74 | return false
75 | default:
76 | throw SwiftFormatsError.parseError
77 | }
78 | }
79 | }
80 |
81 | extension BoolFormatStyle: ParseableFormatStyle {
82 | public var parseStrategy: BoolParseStrategy {
83 | return BoolParseStrategy()
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/DegreesMinutesSecondsNotation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Formats an angle in degrees, minutes, and seconds.
4 | public struct DegreesMinutesSecondsNotationFormatStyle: FormatStyle where FormatInput: BinaryFloatingPoint {
5 |
6 | public enum Mode: Codable {
7 | /// Only the degrees are shown, e.g. "45.25125°".
8 | case decimalDegrees
9 | /// The degrees and minutes are shown, e.g. "45° 15.075'".
10 | case decimalMinutes
11 | /// The degrees, minutes, and seconds are shown, e.g. "45° 15' 4.5".
12 | case decimalSeconds
13 | }
14 |
15 | public static var defaultMeasurementStyle: Measurement.FormatStyle {
16 | .measurement(width: .narrow)
17 | }
18 |
19 | var mode: Mode
20 | var measurementStyle: Measurement.FormatStyle
21 |
22 | public init(mode: DegreesMinutesSecondsNotationFormatStyle.Mode = .decimalDegrees, measurementStyle: Measurement.FormatStyle = Self.defaultMeasurementStyle) {
23 | self.mode = mode
24 | self.measurementStyle = measurementStyle
25 | }
26 |
27 | public func format(_ value: FormatInput) -> String {
28 | let value = Double(value)
29 | switch mode {
30 | case .decimalDegrees:
31 | let degrees = value
32 | return "\(degrees, unit: UnitAngle.degrees, format: measurementStyle)"
33 | case .decimalMinutes:
34 | let degrees = floor(value)
35 | let minutes = (value - degrees) * 60
36 | return "\(degrees, unit: UnitAngle.degrees, format: measurementStyle) \(minutes, unit: UnitAngle.arcMinutes, format: measurementStyle)"
37 | case .decimalSeconds:
38 | let degrees = floor(value)
39 | let minutes = floor(60 * (value - degrees))
40 | let seconds = 3_600 * (value - degrees) - 60 * minutes
41 | return "\(degrees, unit: UnitAngle.degrees, format: measurementStyle) \(minutes, unit: UnitAngle.arcMinutes, format: measurementStyle) \(seconds, unit: UnitAngle.arcSeconds, format: measurementStyle)"
42 | }
43 | }
44 | }
45 |
46 | public extension FormatStyle where Self == DegreesMinutesSecondsNotationFormatStyle {
47 | static func dmsNotation(mode: Self.Mode = .decimalDegrees, measurementStyle: Measurement.FormatStyle = Self.defaultMeasurementStyle) -> Self {
48 | DegreesMinutesSecondsNotationFormatStyle(mode: mode, measurementStyle: measurementStyle)
49 | }
50 | }
51 |
52 | public extension FormatStyle where Self == DegreesMinutesSecondsNotationFormatStyle {
53 | static func dmsNotation(mode: Self.Mode = .decimalDegrees, measurementStyle: Measurement.FormatStyle = Self.defaultMeasurementStyle) -> Self {
54 | DegreesMinutesSecondsNotationFormatStyle(mode: mode, measurementStyle: measurementStyle)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Angles.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// Format angles in degrees or radians, input and output units can be different.
5 | public struct AngleFormatStyle: ParseableFormatStyle where FormatInput: BinaryFloatingPoint {
6 | public static var defaultMeasurementStyle: Measurement.FormatStyle {
7 | .measurement(width: .narrow)
8 | }
9 |
10 | public enum Unit: Codable {
11 | case degrees
12 | case radians
13 | }
14 |
15 | public var inputUnit: Unit
16 | public var outputUnit: Unit
17 | public var measurementStyle: Measurement.FormatStyle
18 | public var locale: Locale
19 |
20 | public var parseStrategy: AngleParseStrategy {
21 | AngleParseStrategy(type: FormatInput.self, defaultInputUnit: inputUnit, outputUnit: outputUnit, locale: locale)
22 | }
23 |
24 | public init(
25 | inputUnit: AngleFormatStyle.Unit,
26 | outputUnit: AngleFormatStyle.Unit,
27 | measurementStyle: Measurement.FormatStyle = Self.defaultMeasurementStyle,
28 | locale: Locale = .autoupdatingCurrent
29 | ) {
30 | self.inputUnit = inputUnit
31 | self.outputUnit = outputUnit
32 | self.measurementStyle = measurementStyle
33 | self.locale = locale
34 | }
35 |
36 | public func format(_ value: FormatInput) -> String {
37 | switch (inputUnit, outputUnit) {
38 | case (.degrees, .degrees):
39 | return "\(Double(value), unit: UnitAngle.degrees, format: measurementStyle.locale(locale))"
40 | case (.radians, .radians):
41 | return "\(Double(value), unit: UnitAngle.radians, format: measurementStyle.locale(locale))"
42 | case (.degrees, .radians):
43 | return "\(degreesToRadians(Double(value)), unit: UnitAngle.radians, format: measurementStyle.locale(locale))"
44 | case (.radians, .degrees):
45 | return "\(radiansToDegrees(Double(value)), unit: UnitAngle.degrees, format: measurementStyle.locale(locale))"
46 | }
47 | }
48 | }
49 |
50 | public struct AngleParseStrategy: ParseStrategy where ParseOutput: BinaryFloatingPoint {
51 |
52 | public typealias Unit = AngleFormatStyle.Unit
53 |
54 | public var defaultInputUnit: Unit?
55 | public var outputUnit: Unit
56 | public var locale: Locale
57 |
58 | init(type: ParseOutput.Type, defaultInputUnit: Unit? = nil, outputUnit: Unit, locale: Locale = .autoupdatingCurrent) {
59 | self.defaultInputUnit = defaultInputUnit
60 | self.outputUnit = outputUnit
61 | self.locale = locale
62 | }
63 |
64 | public func parse(_ value: String) throws -> ParseOutput {
65 | let regex = #/^(.+?)(°|rad)?$/#
66 | guard let match = value.firstMatch(of: regex) else {
67 | throw SwiftFormatsError.parseError
68 | }
69 | let (value, unit) = (try Double(String(match.output.1), format: .number), match.output.2)
70 | let radians: Double
71 | switch (unit, defaultInputUnit) {
72 | case ("°", _), (.none, .degrees):
73 | radians = degreesToRadians(value)
74 | case ("rad", _), (.none, .radians):
75 | radians = value
76 | default:
77 | throw SwiftFormatsError.parseError
78 | }
79 | switch outputUnit {
80 | case .degrees:
81 | return ParseOutput(radiansToDegrees(radians))
82 | case .radians:
83 | return ParseOutput(radians)
84 | }
85 | }
86 | }
87 |
88 | public extension FormatStyle where Self == AngleFormatStyle {
89 | static func angle(inputUnit: Self.Unit, outputUnit: Self.Unit) -> Self {
90 | AngleFormatStyle(inputUnit: inputUnit, outputUnit: outputUnit)
91 | }
92 | }
93 |
94 | public extension ParseStrategy where Self == AngleParseStrategy {
95 | static func angle(defaultInputUnit: AngleFormatStyle.Unit? = nil, outputUnit: AngleFormatStyle.Unit, locale: Locale = .autoupdatingCurrent) -> Self {
96 | Self(type: Double.self, defaultInputUnit: defaultInputUnit, outputUnit: outputUnit, locale: locale)
97 | }
98 | }
99 |
100 | public extension BinaryFloatingPoint {
101 | init(_ value: String, format: AngleFormatStyle) throws {
102 | self = try format.parseStrategy.parse(value)
103 | }
104 | }
105 |
106 | // MARK: -
107 |
108 |
109 | public struct AngleValueFormatStyle: FormatStyle {
110 |
111 | public static var defaultMeasurementStyle: Measurement.FormatStyle {
112 | .measurement(width: .narrow)
113 | }
114 |
115 | public enum Unit: Codable {
116 | case degrees
117 | case radians
118 | }
119 |
120 | public var unit: Unit
121 | public var measurementStyle: Measurement.FormatStyle
122 | public var locale: Locale
123 | public var defaultInputUnit: Unit? = .degrees
124 |
125 | public init(unit: AngleValueFormatStyle.Unit, measurementStyle: Measurement.FormatStyle = Self.defaultMeasurementStyle, locale: Locale = .autoupdatingCurrent) {
126 | self.unit = unit
127 | self.measurementStyle = measurementStyle
128 | self.locale = locale
129 | }
130 |
131 | public func format(_ value: Angle) -> String {
132 | switch unit {
133 | case .degrees:
134 | return "\(value.degrees, unit: UnitAngle.degrees, format: measurementStyle.locale(locale))"
135 | case .radians:
136 | return "\(value.radians, unit: UnitAngle.radians, format: measurementStyle.locale(locale))"
137 | }
138 | }
139 | }
140 |
141 | public extension AngleValueFormatStyle {
142 | func unit(_ unit: Unit) -> Self {
143 | var copy = self
144 | copy.unit = unit
145 | return copy
146 | }
147 |
148 | var radians: Self {
149 | var copy = self
150 | copy.unit = .radians
151 | return copy
152 | }
153 |
154 | var degrees: Self {
155 | var copy = self
156 | copy.unit = .degrees
157 | return copy
158 | }
159 | }
160 |
161 | public extension FormatStyle where Self == AngleValueFormatStyle {
162 | static var angle: Self {
163 | AngleValueFormatStyle(unit: .degrees)
164 | }
165 | }
166 |
167 | public extension Angle {
168 | func formatted() -> String {
169 | return AngleValueFormatStyle(unit: .degrees, measurementStyle: AngleValueFormatStyle.defaultMeasurementStyle, locale: .autoupdatingCurrent).format(self)
170 | }
171 | }
172 |
173 | // MARK: -
174 |
175 | extension AngleValueFormatStyle: ParseableFormatStyle {
176 |
177 | public var parseStrategy: AngleValueParseStrategy {
178 | return AngleValueParseStrategy(defaultInputUnit: defaultInputUnit)
179 | }
180 |
181 | public func defaultInputUnit(_ defaultInputUnit: Unit) -> Self {
182 | var copy = self
183 | copy.defaultInputUnit = defaultInputUnit
184 | return copy
185 | }
186 | }
187 |
188 | public struct AngleValueParseStrategy: ParseStrategy {
189 | public typealias Unit = AngleValueFormatStyle.Unit
190 |
191 | public var defaultInputUnit: Unit?
192 |
193 | public init(defaultInputUnit: AngleValueParseStrategy.Unit? = .degrees) {
194 | self.defaultInputUnit = defaultInputUnit
195 | }
196 |
197 | public func parse(_ value: String) throws -> Angle {
198 | let regex = #/^\s*(.+?)\s*(°|rad)?\s*$/#
199 | guard let match = value.firstMatch(of: regex) else {
200 | throw SwiftFormatsError.parseError
201 | }
202 | let (value, unit) = (try Double(String(match.output.1), format: .number), match.output.2)
203 | switch (unit, defaultInputUnit) {
204 | case ("°", _), (nil, .degrees):
205 | return Angle(degrees: value)
206 | case ("rad", _), (nil, .radians):
207 | return Angle(radians: value)
208 | default:
209 | throw SwiftFormatsError.parseError
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+ClosedRange.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import RegexBuilder
3 |
4 | /// Formats a `ClosedRange`.
5 | public struct ClosedRangeFormatStyle : FormatStyle where Bound: Comparable, Substyle: FormatStyle, Substyle.FormatInput == Bound, Substyle.FormatOutput == String {
6 |
7 | /// The `FormatStyle` used to format the individual bounds of the `ClosedRange`.
8 | public var substyle: Substyle
9 |
10 | /// The delimiter used to separate the bounds of the `ClosedRange`.
11 | public var delimiter: String? = "..."
12 |
13 | /// - Parameters:
14 | /// - substyle: The `FormatStyle` used to format the individual bounds of the `ClosedRange`.
15 | /// - delimiter: The delimiter used to separate the bounds of the `ClosedRange`.
16 | public init(substyle: Substyle, delimiter: String? = "...") {
17 | self.substyle = substyle
18 | self.delimiter = delimiter
19 | }
20 |
21 | public func format(_ value: ClosedRange) -> String {
22 | let parts = [
23 | substyle.format(value.lowerBound),
24 | delimiter,
25 | substyle.format(value.upperBound),
26 | ]
27 | return parts
28 | .compactMap { $0 }
29 | .joined(separator: " ")
30 | }
31 | }
32 |
33 | extension ClosedRangeFormatStyle: ParseableFormatStyle where Substyle: ParseableFormatStyle {
34 | public var parseStrategy: ClosedRangeParseStrategy {
35 | return ClosedRangeParseStrategy(substrategy: substyle.parseStrategy)
36 | }
37 | }
38 |
39 | /// Parses a string into a `ClosedRange`.
40 | public struct ClosedRangeParseStrategy : ParseStrategy where Bound: Comparable, Substrategy: ParseStrategy, Substrategy.ParseInput == String, Substrategy.ParseOutput == Bound {
41 |
42 | /// The `ParseStrategy` used to parse the individual bounds of the `ClosedRange`.
43 | public var substrategy: Substrategy
44 |
45 | /// The delimiters used to separate the bounds of the `ClosedRange`.
46 | public var delimiters: [String]
47 |
48 | /// - Parameters:
49 | /// - substrategy: The `ParseStrategy` used to parse the individual bounds of the `ClosedRange`.
50 | /// - delimiters: The delimiters used to separate the bounds of the `ClosedRange`.
51 | public init(substrategy: Substrategy, delimiters: [String] = ["...", "-"]) {
52 | self.substrategy = substrategy
53 | self.delimiters = delimiters
54 | }
55 |
56 | public func parse(_ value: String) throws -> ClosedRange {
57 | // (?.+?)\s*(?:\.\.\.|\-)\s*(?.+?)
58 | let lowerBoundReference = Reference(Substring.self)
59 | let upperBoundReference = Reference(Substring.self)
60 | let pattern = Regex {
61 | Capture(as: lowerBoundReference) {
62 | OneOrMore(.any)
63 | }
64 | ZeroOrMore(.whitespace)
65 | delimiters
66 | ZeroOrMore(.whitespace)
67 | Capture(as: upperBoundReference) {
68 | OneOrMore(.any)
69 | }
70 | }
71 |
72 | guard let match = value.firstMatch(of: pattern) else {
73 | throw SwiftFormatsError.parseError
74 | }
75 |
76 | let lowerBound = match[lowerBoundReference]
77 | let upperBound = match[upperBoundReference]
78 |
79 | return try substrategy.parse(String(lowerBound)) ... substrategy.parse(String(upperBound))
80 | }
81 | }
82 |
83 | public extension ClosedRangeParseStrategy {
84 | func delimiters(_ delimiters: [String]) -> Self {
85 | var copy = self
86 | copy.delimiters = delimiters
87 | return copy
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Coordinates.swift:
--------------------------------------------------------------------------------
1 | import CoreLocation
2 | import Foundation
3 |
4 | // TODO: add support for UTM (can import https://github.com/wtw-software/UTMConversion)
5 | // TODO: add support for geohash
6 |
7 | /// A format style for a lat/long coordinate.
8 | public struct CoordinatesFormatter: FormatStyle {
9 | // TODO: need a way to pass options down to sub styles
10 |
11 | public func format(_ value: CLLocationCoordinate2D) -> String {
12 | "\(value.latitude, format: .latitude()), \(value.longitude, format: .longitude())"
13 | }
14 | }
15 |
16 | public extension FormatStyle where Self == CoordinatesFormatter {
17 | static var coordinates: Self {
18 | CoordinatesFormatter()
19 | }
20 | }
21 |
22 | // MARK: -
23 |
24 | /// Format latitude values, including hemisphere.
25 | public struct LatitudeFormatStyle: FormatStyle where FormatInput: BinaryFloatingPoint {
26 | public typealias Substyle = DegreesMinutesSecondsNotationFormatStyle
27 |
28 | public static var defaultSubstyle: Substyle {
29 | .init()
30 | }
31 |
32 | public var includeHemisphere: Bool
33 | public var substyle: Substyle
34 |
35 | public init(includeHemisphere: Bool = true, substyle: Substyle = Self.defaultSubstyle) {
36 | self.includeHemisphere = includeHemisphere
37 | self.substyle = substyle
38 | }
39 |
40 | public func format(_ value: FormatInput) -> String {
41 | if includeHemisphere {
42 | // TODO: Need a flipped flag
43 | return "\(value, format: substyle) \(Hemisphere(latitude: Double(value)), format: .hemisphere())"
44 | }
45 | else {
46 | return "\(value, format: substyle)"
47 | }
48 | }
49 | }
50 |
51 | public extension FormatStyle where Self == LatitudeFormatStyle {
52 | static func latitude(includeHemisphere: Bool = true, substyle: Self.Substyle = Self.defaultSubstyle) -> Self {
53 | .init(includeHemisphere: includeHemisphere, substyle: substyle)
54 | }
55 | }
56 |
57 | public extension FormatStyle where Self == LatitudeFormatStyle {
58 | static func latitude(includeHemisphere: Bool = true, substyle: Self.Substyle = Self.defaultSubstyle) -> Self {
59 | .init(includeHemisphere: includeHemisphere, substyle: substyle)
60 | }
61 | }
62 |
63 | // MARK: -
64 |
65 | /// Format latitude values, including hemisphere.
66 | public struct LongitudeFormatStyle: FormatStyle where FormatInput: BinaryFloatingPoint {
67 | public typealias Substyle = DegreesMinutesSecondsNotationFormatStyle
68 |
69 | public static var defaultSubstyle: Substyle {
70 | .init()
71 | }
72 |
73 | public var includeHemisphere: Bool
74 | public var substyle: Substyle
75 |
76 | public init(includeHemisphere: Bool = true, substyle: Substyle = Self.defaultSubstyle) {
77 | self.includeHemisphere = includeHemisphere
78 | self.substyle = substyle
79 | }
80 |
81 | public func format(_ value: FormatInput) -> String {
82 | if includeHemisphere {
83 | // TODO: Need a flipped flag
84 | return "\(value, format: substyle) \(Hemisphere(longitude: Double(value)), format: .hemisphere())"
85 | }
86 | else {
87 | return "\(value, format: substyle)"
88 | }
89 | }
90 | }
91 |
92 | public extension FormatStyle where Self == LongitudeFormatStyle {
93 | static func longitude(includeHemisphere: Bool = true, substyle: Self.Substyle = Self.defaultSubstyle) -> Self {
94 | .init(includeHemisphere: includeHemisphere, substyle: substyle)
95 | }
96 | }
97 |
98 | public extension FormatStyle where Self == LongitudeFormatStyle {
99 | static func longitude(includeHemisphere: Bool = true, substyle: Self.Substyle = Self.defaultSubstyle) -> Self {
100 | .init(includeHemisphere: includeHemisphere, substyle: substyle)
101 | }
102 | }
103 |
104 |
105 | // MARK: -
106 |
107 | internal enum LatitudeLongitude: CaseIterable {
108 | case latitude
109 | case longitude
110 | }
111 |
112 | internal enum Hemisphere: CaseIterable {
113 | case east
114 | case west
115 | case north
116 | case south
117 | }
118 |
119 | internal extension Hemisphere {
120 | init(latitude value: CLLocationDegrees) {
121 | self = value >= 0 ? .north : .south
122 | }
123 |
124 | init(longitude value: CLLocationDegrees) {
125 | self = value >= 0 ? .east : .west
126 | }
127 | }
128 |
129 | // TODO: Localized
130 | // TODO: Really should be `cardinal direction`
131 | extension Hemisphere: CustomStringConvertible {
132 | var description: String {
133 | switch self {
134 | case .north:
135 | return "North"
136 | case .south:
137 | return "South"
138 | case .east:
139 | return "East"
140 | case .west:
141 | return "West"
142 | }
143 | }
144 | }
145 |
146 | internal struct HemisphereFormatStyle: FormatStyle {
147 | let abbreviated: Bool
148 |
149 | init(abbreviated: Bool) {
150 | self.abbreviated = abbreviated
151 | }
152 |
153 | func format(_ value: Hemisphere) -> String {
154 | abbreviated ? "\(value.description.first!)" : value.description
155 | }
156 | }
157 |
158 | internal extension FormatStyle where Self == HemisphereFormatStyle {
159 | static func hemisphere(abbreviated: Bool = true) -> Self {
160 | HemisphereFormatStyle(abbreviated: abbreviated)
161 | }
162 | }
163 |
164 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+CoreGraphics.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 | import Foundation
3 |
4 | // MARK: CGPointFormatStyle
5 |
6 | public struct CGPointFormatStyle: ParseableFormatStyle {
7 |
8 | public var parseStrategy: CGPointParseStrategy {
9 | return CGPointParseStrategy(componentFormat: componentFormat)
10 | }
11 |
12 | public var componentFormat: FloatingPointFormatStyle = .number
13 |
14 | public init() {
15 | }
16 |
17 | public func format(_ value: CGPoint) -> String {
18 | let style = SimpleListFormatStyle(substyle: componentFormat)
19 | return style.format([value.x, value.y])
20 | }
21 | }
22 |
23 | public extension FormatStyle where Self == CGPointFormatStyle {
24 | static var point: Self {
25 | return Self()
26 | }
27 | }
28 |
29 | // MARK: CGPointParseStrategy
30 |
31 | public struct CGPointParseStrategy: ParseStrategy {
32 | public var componentFormat: FloatingPointFormatStyle
33 |
34 | public init(componentFormat: FloatingPointFormatStyle = .number) {
35 | self.componentFormat = componentFormat
36 | }
37 |
38 | public func parse(_ value: String) throws -> CGPoint {
39 | var strategy = SimpleListFormatStyle(substyle: componentFormat).parseStrategy
40 | strategy.countRange = 2 ... 2
41 | let scalars = try strategy.parse(value)
42 | return CGPoint(x: scalars[0], y: scalars[1])
43 | }
44 | }
45 |
46 | // MARK: CGPoint convenience constructors
47 |
48 | public extension CGPoint {
49 | init(_ string: String) throws {
50 | self = try CGPointFormatStyle().parseStrategy.parse(string)
51 | }
52 |
53 | init(_ input: ParseInput, format: Format) throws where Format: ParseableFormatStyle, ParseInput: StringProtocol, Format.Strategy == CGPointParseStrategy {
54 | self = try format.parseStrategy.parse(String(input))
55 | }
56 |
57 | init(_ value: Value, strategy: Strategy) throws where Strategy: ParseStrategy, Value: StringProtocol, Strategy.ParseInput == String, Strategy.ParseOutput == CGPoint {
58 | self = try strategy.parse(String(value))
59 | }
60 | }
61 |
62 | public extension ParseableFormatStyle where Self == CGPointFormatStyle {
63 | static var point: Self {
64 | .init()
65 | }
66 | }
67 |
68 | public extension ParseStrategy where Self == CGPointParseStrategy {
69 | static var point: Self {
70 | .init()
71 | }
72 | }
73 |
74 | // MARK: CGPoint.formatted
75 |
76 | public extension CGPoint {
77 | func formatted(_ format: S) -> S.FormatOutput where Self == S.FormatInput, S: FormatStyle {
78 | return format.format(self)
79 | }
80 |
81 | func formatted() -> String {
82 | formatted(CGPointFormatStyle())
83 | }
84 | }
85 |
86 | // MARK: CGSizeFormatStyle
87 |
88 | /// Format `CGSize`s.
89 | public struct CGSizeFormatStyle: ParseableFormatStyle {
90 |
91 | public var parseStrategy: CGSizeParseStrategy {
92 | return CGSizeParseStrategy(componentFormat: componentFormat)
93 | }
94 |
95 | public var componentFormat: FloatingPointFormatStyle = .number
96 |
97 | public init() {
98 | }
99 |
100 | public func format(_ value: CGSize) -> String {
101 | let style = SimpleListFormatStyle(substyle: componentFormat)
102 | return style.format([value.width, value.height])
103 | }
104 | }
105 |
106 | public extension FormatStyle where Self == CGSizeFormatStyle {
107 | static var size: Self {
108 | return Self()
109 | }
110 | }
111 |
112 | // MARK: CGSizeParseStrategy
113 |
114 | public struct CGSizeParseStrategy: ParseStrategy {
115 | public var componentFormat: FloatingPointFormatStyle
116 |
117 | public init(componentFormat: FloatingPointFormatStyle = .number) {
118 | self.componentFormat = componentFormat
119 | }
120 |
121 | public func parse(_ value: String) throws -> CGSize {
122 | var strategy = SimpleListFormatStyle(substyle: componentFormat).parseStrategy
123 | strategy.countRange = 2 ... 2
124 | let scalars = try strategy.parse(value)
125 | return CGSize(width: scalars[0], height: scalars[1])
126 | }
127 | }
128 |
129 | // MARK: CGSize convenience init
130 |
131 | public extension CGSize {
132 | init(_ string: String) throws {
133 | self = try CGSizeFormatStyle().parseStrategy.parse(string)
134 | }
135 |
136 | init(_ input: ParseInput, format: Format) throws where Format: ParseableFormatStyle, ParseInput: StringProtocol, Format.Strategy == CGSizeParseStrategy {
137 | self = try format.parseStrategy.parse(String(input))
138 | }
139 |
140 | init(_ value: Value, strategy: Strategy) throws where Strategy: ParseStrategy, Value: StringProtocol, Strategy.ParseInput == String, Strategy.ParseOutput == CGSize {
141 | self = try strategy.parse(String(value))
142 | }
143 | }
144 |
145 | public extension ParseableFormatStyle where Self == CGSizeFormatStyle {
146 | static var point: Self {
147 | .init()
148 | }
149 | }
150 |
151 | public extension ParseStrategy where Self == CGSizeParseStrategy {
152 | static var point: Self {
153 | .init()
154 | }
155 | }
156 |
157 | // MARK: CGSize.formatted
158 |
159 | public extension CGSize {
160 | func formatted(_ format: S) -> S.FormatOutput where Self == S.FormatInput, S: FormatStyle {
161 | return format.format(self)
162 | }
163 |
164 | func formatted() -> String {
165 | formatted(CGSizeFormatStyle())
166 | }
167 | }
168 |
169 | // MARK: CGRectFormatStyle
170 |
171 | public struct CGRectFormatStyle: FormatStyle {
172 |
173 | public var componentFormat: FloatingPointFormatStyle = .number
174 |
175 | public init() {
176 | }
177 |
178 | public func format(_ value: CGRect) -> String {
179 | let style = MappingFormatStyle(valueStyle: componentFormat)
180 | return style.format([("x", value.origin.x), ("y", value.origin.y), ("width", value.size.width), ("height", value.size.height)])
181 | }
182 | }
183 |
184 | public extension FormatStyle where Self == CGRectFormatStyle {
185 | static var rectangle: Self {
186 | return Self()
187 | }
188 | }
189 |
190 | // MARK: CGRect.formatted
191 |
192 | public extension CGRect {
193 | func formatted(_ format: S) -> S.FormatOutput where Self == S.FormatInput, S: FormatStyle {
194 | return format.format(self)
195 | }
196 |
197 | func formatted() -> String {
198 | formatted(CGRectFormatStyle())
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/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+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 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Hexdump.swift:
--------------------------------------------------------------------------------
1 | import Algorithms
2 | import Foundation
3 |
4 | // TODO: Remove FormatInput.Index == Int restriction
5 |
6 | /// Hex dump `DataProtocol``
7 | public struct HexdumpFormatStyle: FormatStyle where FormatInput: DataProtocol, FormatInput.Index == Int {
8 |
9 | public typealias FormatInput = FormatInput
10 | public typealias FormatOutput = String
11 |
12 | public var width: Int
13 | public var baseAddress: Int
14 | public var separator: String
15 | public var terminator: String
16 |
17 | /// - Parameters:
18 | /// - width: Number of octets per line.
19 | /// - baseAddress: Base address to use for the first line.
20 | /// - separator: Separator string to use between lines.
21 | /// - terminator: Terminator string to use after the last line.
22 | public init(width: Int = 16, baseAddress: Int = 0, separator: String = "\n", terminator: String = "") {
23 | self.width = width
24 | self.baseAddress = baseAddress
25 | self.separator = separator
26 | self.terminator = terminator
27 | }
28 |
29 | public func format(_ buffer: FormatInput) -> String {
30 | var string = ""
31 | for index in stride(from: 0, through: buffer.count, by: width) {
32 | let address = UInt(baseAddress + index).formatted(.hex.leadingZeros().prefix(.none))
33 |
34 | let chunk = buffer[index ..< (index + min(width, buffer.count - index))]
35 | if chunk.isEmpty {
36 | break
37 | }
38 | let hex = chunk.map { $0.formatted(.hex.leadingZeros()) }
39 | .joined(separator: " ")
40 | .padding(toLength: width * 3 - 1, withPad: " ", startingAt: 0)
41 |
42 | let part = chunk.map { (c: UInt8) -> String in
43 | let scalar = UnicodeScalar(c)
44 | let character = Character(scalar)
45 | return isprint(Int32(c)) != 0 ? String(character) : "?"
46 | }
47 | .joined()
48 |
49 | string.write("\(address) \(hex) \(part)")
50 | string.write(separator)
51 | }
52 | string.write(terminator)
53 | return string
54 | }
55 | }
56 |
57 | public extension FormatStyle where Self == HexdumpFormatStyle {
58 | static func hexdump(width: Int = 16, baseAddress: Int = 0, separator: String = "\n", terminator: String = "") -> Self {
59 | HexdumpFormatStyle(width: width, baseAddress: baseAddress, separator: separator, terminator: terminator)
60 | }
61 | }
62 |
63 | public extension FormatStyle where Self == HexdumpFormatStyle<[UInt8]> {
64 | static func hexdump(width: Int = 16, baseAddress: Int = 0, separator: String = "\n", terminator: String = "") -> Self {
65 | HexdumpFormatStyle(width: width, baseAddress: baseAddress, separator: separator, terminator: terminator)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/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/FormatStyle+Matrix.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import simd
3 |
4 | public protocol FormattableMatrix: Equatable {
5 | associatedtype Scalar: SIMDScalar
6 |
7 | var columnCount: Int { get }
8 | var rowCount: Int { get }
9 |
10 | init()
11 |
12 | subscript(column: Int, row: Int) -> Scalar { get set }
13 | }
14 |
15 | public enum MatrixOrder: Codable {
16 | case columnMajor
17 | case rowMajor
18 | }
19 |
20 | public struct MatrixFormatStyle : FormatStyle where Matrix: FormattableMatrix, ScalarStyle: FormatStyle, ScalarStyle.FormatInput == Matrix.Scalar, ScalarStyle.FormatOutput == String {
21 |
22 | public var order: MatrixOrder
23 | public var scalarStyle: ScalarStyle
24 |
25 | public init(type: FormatInput.Type, order: MatrixOrder = .rowMajor, scalarStyle: ScalarStyle) {
26 | self.order = order
27 | self.scalarStyle = scalarStyle
28 | }
29 |
30 | public func format(_ value: Matrix) -> String {
31 | let elements: [[Matrix.Scalar]]
32 | switch order {
33 | case .columnMajor:
34 | elements = (0 ..< value.columnCount).map { column in
35 | return (0 ..< value.rowCount).map { row in
36 | value[column, row]
37 | }
38 | }
39 | case.rowMajor:
40 | elements = (0 ..< value.rowCount).map { row in
41 | return (0 ..< value.columnCount).map { column in
42 | value[column, row]
43 | }
44 | }
45 | }
46 | return SimpleListFormatStyle(substyle: SimpleListFormatStyle(substyle: scalarStyle), separator: "\n").format(elements)
47 | }
48 | }
49 |
50 | public extension MatrixFormatStyle {
51 |
52 | func scalarStyle(_ order: MatrixOrder) -> Self {
53 | var copy = self
54 | copy.order = order
55 | return copy
56 | }
57 |
58 | func scalarStyle(_ scalarStyle: ScalarStyle) -> Self {
59 | var copy = self
60 | copy.scalarStyle = scalarStyle
61 | return copy
62 | }
63 | }
64 |
65 | // MARK: -
66 |
67 | extension MatrixFormatStyle: ParseableFormatStyle where ScalarStyle: ParseableFormatStyle {
68 | public var parseStrategy: MatrixParseStrategy {
69 | return MatrixParseStrategy(order: order, scalarStrategy: scalarStyle.parseStrategy)
70 | }
71 | }
72 |
73 | public struct MatrixParseStrategy : ParseStrategy where Matrix: FormattableMatrix, ScalarStrategy: ParseStrategy, ScalarStrategy.ParseInput == String, ScalarStrategy.ParseOutput == Matrix.Scalar {
74 |
75 | public var order: MatrixOrder
76 | public var scalarStrategy: ScalarStrategy
77 |
78 | public init(order: MatrixOrder = .rowMajor, scalarStrategy: ScalarStrategy) {
79 | self.order = order
80 | self.scalarStrategy = scalarStrategy
81 | }
82 |
83 | public func parse(_ value: String) throws -> Matrix {
84 | var result = Matrix()
85 | let innerStrategy = SimpleListParseStrategy(substrategy: scalarStrategy, separator: ",", countRange: result.rowCount ... result.rowCount)
86 | let elementsStrategy = SimpleListParseStrategy(substrategy: innerStrategy, separator: "\n", countRange: result.columnCount ... result.columnCount)
87 | let elements = try elementsStrategy.parse(value)
88 | switch order {
89 | case .columnMajor:
90 | for column in 0 ..< result.columnCount {
91 | for row in 0 ..< result.rowCount {
92 | result[row, column] = elements[row][column]
93 | }
94 | }
95 | case .rowMajor:
96 | for column in 0 ..< result.columnCount {
97 | for row in 0 ..< result.rowCount {
98 | result[row, column] = elements[column][row]
99 | }
100 | }
101 | }
102 | return result
103 | }
104 | }
105 |
106 | // MARK: -
107 |
108 | public extension FormatStyle where Self == MatrixFormatStyle> {
109 | static var matrix: Self {
110 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
111 | }
112 | }
113 |
114 | public extension FormatStyle where Self == MatrixFormatStyle> {
115 | static var matrix: Self {
116 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
117 | }
118 | }
119 |
120 | public extension FormatStyle where Self == MatrixFormatStyle> {
121 | static var matrix: Self {
122 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
123 | }
124 | }
125 |
126 | public extension FormatStyle where Self == MatrixFormatStyle> {
127 | static var matrix: Self {
128 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
129 | }
130 | }
131 |
132 | public extension FormatStyle where Self == MatrixFormatStyle> {
133 | static var matrix: Self {
134 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
135 | }
136 | }
137 |
138 | public extension FormatStyle where Self == MatrixFormatStyle> {
139 | static var matrix: Self {
140 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
141 | }
142 | }
143 |
144 | public extension FormatStyle where Self == MatrixFormatStyle> {
145 | static var matrix: Self {
146 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
147 | }
148 | }
149 |
150 | public extension FormatStyle where Self == MatrixFormatStyle> {
151 | static var matrix: Self {
152 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
153 | }
154 | }
155 |
156 | public extension FormatStyle where Self == MatrixFormatStyle> {
157 | static var matrix: Self {
158 | return MatrixFormatStyle(type: FormatInput.self, scalarStyle: .number)
159 | }
160 | }
161 |
162 | // MARK: -
163 |
164 | extension simd_float2x2: FormattableMatrix {
165 | public var columnCount: Int { return 2 }
166 | public var rowCount: Int { return 2 }
167 | }
168 | extension simd_float3x2: FormattableMatrix {
169 | public var columnCount: Int { return 3 }
170 | public var rowCount: Int { return 2 }
171 | }
172 | extension simd_float4x2: FormattableMatrix {
173 | public var columnCount: Int { return 4 }
174 | public var rowCount: Int { return 2 }
175 | }
176 | extension simd_float2x3: FormattableMatrix {
177 | public var columnCount: Int { return 2 }
178 | public var rowCount: Int { return 3 }
179 | }
180 | extension simd_float3x3: FormattableMatrix {
181 | public var columnCount: Int { return 3 }
182 | public var rowCount: Int { return 3 }
183 | }
184 | extension simd_float4x3: FormattableMatrix {
185 | public var columnCount: Int { return 4 }
186 | public var rowCount: Int { return 3 }
187 | }
188 | extension simd_float2x4: FormattableMatrix {
189 | public var columnCount: Int { return 2 }
190 | public var rowCount: Int { return 4 }
191 | }
192 | extension simd_float3x4: FormattableMatrix {
193 | public var columnCount: Int { return 3 }
194 | public var rowCount: Int { return 4 }
195 | }
196 | extension simd_float4x4: FormattableMatrix {
197 | public var columnCount: Int { return 4 }
198 | public var rowCount: Int { return 4 }
199 | }
200 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Quaternion.swift:
--------------------------------------------------------------------------------
1 | import RegexBuilder
2 | import Foundation
3 | import simd
4 |
5 | public protocol FormattableQuaternion: Equatable {
6 | associatedtype Scalar: SIMDScalar, BinaryFloatingPoint
7 |
8 | var real: Scalar { get }
9 | var imag: SIMD3 { get }
10 |
11 | var angle: Scalar { get }
12 | var axis: SIMD3 { get }
13 |
14 | var vector: SIMD4 { get }
15 |
16 | init(real: Scalar, imag: SIMD3)
17 | init(vector: SIMD4)
18 | init(angle: Scalar, axis: SIMD3)
19 | }
20 |
21 | internal extension FormattableQuaternion {
22 | static var identity: Self {
23 | Self(real: 1, imag: .zero)
24 | }
25 | }
26 |
27 | // MARK: -
28 |
29 | public struct QuaternionFormatStyle : FormatStyle where Q: FormattableQuaternion {
30 |
31 | public enum Style: Codable, CaseIterable {
32 | case components // ix, iy, iz, r
33 | case vector // x, y, z, w
34 | case angleAxis // angle, axis x, axis y, axis z
35 | }
36 |
37 | public var style: Style
38 | public var compositeStyle: CompositeStyle
39 | public var isHumanReadable: Bool // TODO: rename
40 | public typealias NumberStyle = FloatingPointFormatStyle
41 | public var numberStyle: NumberStyle
42 |
43 | public init(type: Q.Type, style: QuaternionFormatStyle.Style = .components, compositeStyle: CompositeStyle = .mapping, isHumanReadable: Bool = true, numberStyle: NumberStyle = NumberStyle()) {
44 | self.style = style
45 | self.compositeStyle = compositeStyle
46 | self.isHumanReadable = isHumanReadable
47 | self.numberStyle = numberStyle
48 | }
49 |
50 | public func format(_ value: Q) -> String {
51 | if value == .identity && isHumanReadable {
52 | return "identity"
53 | }
54 |
55 | switch style {
56 | case .components:
57 | let mapping = [
58 | ("real", value.real),
59 | ("ix", value.imag.x),
60 | ("iy", value.imag.y),
61 | ("iz", value.imag.z),
62 | ]
63 | return MappingFormatStyle(keyStyle: IdentityFormatStyle(), valueStyle: numberStyle).format(mapping)
64 | case .vector:
65 | return VectorFormatStyle(type: SIMD4.self, scalarStyle: numberStyle).format(value.vector)
66 | case .angleAxis:
67 | let mapping = [
68 | ("angle", value.angle),
69 | ("x", value.axis.x),
70 | ("y", value.axis.y),
71 | ("z", value.axis.z),
72 | ]
73 | return MappingFormatStyle(keyStyle: IdentityFormatStyle(), valueStyle: numberStyle).format(mapping)
74 | }
75 | }
76 | }
77 |
78 | public extension QuaternionFormatStyle {
79 | func style(_ style: Style) -> Self {
80 | var copy = self
81 | copy.style = style
82 | return copy
83 | }
84 |
85 | func compositeStyle(_ compositeStyle: CompositeStyle) -> Self {
86 | var copy = self
87 | copy.compositeStyle = compositeStyle
88 | return copy
89 | }
90 |
91 | func isHumanReadable(_ isHumanReadable: Bool) -> Self {
92 | var copy = self
93 | copy.isHumanReadable = isHumanReadable
94 | return copy
95 | }
96 |
97 | func numberStyle(_ numberStyle: FloatingPointFormatStyle) -> Self {
98 | var copy = self
99 | copy.numberStyle = numberStyle
100 | return copy
101 | }
102 | }
103 |
104 | public extension FormatStyle where Self == QuaternionFormatStyle {
105 | static var quaternion: Self {
106 | return QuaternionFormatStyle(type: simd_quatf.self)
107 | }
108 | }
109 |
110 | public extension FormatStyle where Self == QuaternionFormatStyle {
111 | static var quaternion: Self {
112 | return QuaternionFormatStyle(type: simd_quatd.self)
113 | }
114 | }
115 |
116 | // MARK: -
117 |
118 | public struct QuaternionParseStrategy : ParseStrategy where Q: FormattableQuaternion {
119 | public typealias Style = QuaternionFormatStyle.Style
120 | public typealias NumberStrategy = FloatingPointParseStrategy>
121 |
122 | public var style: Style
123 | public var compositeStyle: CompositeStyle
124 | public var isHumanReadable: Bool
125 | public var numberStrategy: NumberStrategy
126 |
127 | public init(type: Q.Type, style: QuaternionParseStrategy.Style = .components, compositeStyle: CompositeStyle = .mapping, isHumanReadable: Bool = true, numberStrategy: NumberStrategy = FloatingPointFormatStyle().parseStrategy) {
128 | self.style = style
129 | self.compositeStyle = compositeStyle
130 | self.isHumanReadable = isHumanReadable
131 | self.numberStrategy = numberStrategy
132 | }
133 |
134 | public func parse(_ value: String) throws -> Q {
135 | let value = value.trimmingCharacters(in: .whitespaces)
136 | if value.lowercased() == "identity" {
137 | return Q.identity
138 | }
139 | switch style {
140 | case .components: // ix, iy, iz, real
141 | let mapping = try MappingParseStrategy(keyStrategy: IdentityParseStategy(), valueStrategy: numberStrategy).parse(value)
142 | let dictionary = Dictionary(uniqueKeysWithValues: mapping)
143 | guard let ix = dictionary["ix"], let iy = dictionary["iy"], let iz = dictionary["iz"], let real = dictionary["real"] else {
144 | throw SwiftFormatsError.missingKeys
145 | }
146 | return Q(real: real, imag: [ix, iy, iz])
147 | case .vector: // x, y, z, w
148 | let vector: SIMD4 = try VectorParseStrategy(scalarStrategy: numberStrategy, compositeStyle: compositeStyle).parse(value)
149 | return Q(vector: vector)
150 | case .angleAxis: // angle, axis x, axis y, axis z
151 | let mapping = try MappingParseStrategy(keyStrategy: IdentityParseStategy(), valueStrategy: numberStrategy).parse(value)
152 | let dictionary = Dictionary(uniqueKeysWithValues: mapping)
153 | guard let angle = dictionary["angle"], let x = dictionary["x"], let y = dictionary["y"], let z = dictionary["z"] else {
154 | throw SwiftFormatsError.missingKeys
155 | }
156 | return Q(angle: angle, axis: [x, y, z])
157 | }
158 | }
159 | }
160 |
161 | extension QuaternionFormatStyle: ParseableFormatStyle {
162 | public var parseStrategy: QuaternionParseStrategy {
163 | return QuaternionParseStrategy(type: Q.self, style: style, compositeStyle: compositeStyle, isHumanReadable: isHumanReadable, numberStrategy: numberStyle.parseStrategy)
164 | }
165 | }
166 |
167 | // MARK: -
168 |
169 | extension simd_quatf: FormattableQuaternion {
170 | }
171 |
172 | extension simd_quatd: FormattableQuaternion {
173 | }
174 |
175 | public extension FormattableQuaternion {
176 |
177 | func formatted(_ format: S) -> S.FormatOutput where Self == S.FormatInput, S: FormatStyle {
178 | return format.format(self)
179 | }
180 |
181 | func formatted() -> String {
182 | return self.formatted(QuaternionFormatStyle(type: Self.self))
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/FormatStyle+Vector.swift:
--------------------------------------------------------------------------------
1 | import RegexBuilder
2 | import Foundation
3 | import simd
4 |
5 | // TOPO: Move somewhere common
6 | public enum CompositeStyle: Codable {
7 | case list
8 | case mapping
9 | }
10 |
11 | public struct VectorFormatStyle : FormatStyle where V: SIMD, ScalarStyle: FormatStyle, ScalarStyle.FormatInput == V.Scalar, ScalarStyle.FormatOutput == String {
12 | public var scalarStyle: ScalarStyle
13 | public var compositeStyle: CompositeStyle
14 | public var scalarNames = ["x", "y", "z", "w"] // TODO: Localize, allow changing of names, e.g. rgba or quaternion fields
15 |
16 | public init(scalarStyle: ScalarStyle, compositeStyle: CompositeStyle = .mapping) {
17 | self.scalarStyle = scalarStyle
18 | self.compositeStyle = compositeStyle
19 | }
20 |
21 | public func format(_ value: V) -> String {
22 | switch compositeStyle {
23 | case .list:
24 | return SimpleListFormatStyle(substyle: scalarStyle).format(value.scalars)
25 | case .mapping:
26 | let mapping = Array(zip(scalarNames, value.scalars))
27 | return MappingFormatStyle(valueStyle: scalarStyle).format(mapping)
28 | }
29 | }
30 | }
31 |
32 | extension VectorFormatStyle {
33 | public init(type: V.Type, scalarStyle: ScalarStyle, compositeStyle: CompositeStyle = .mapping) {
34 | self.init(scalarStyle: scalarStyle, compositeStyle: compositeStyle)
35 | }
36 |
37 | }
38 |
39 | public extension VectorFormatStyle {
40 | func scalarStyle(_ scalarStyle: ScalarStyle) -> Self {
41 | var copy = self
42 | copy.scalarStyle = scalarStyle
43 | return copy
44 | }
45 |
46 | func compositeStyle(_ compositeStyle: CompositeStyle) -> Self {
47 | var copy = self
48 | copy.compositeStyle = compositeStyle
49 | return copy
50 | }
51 | }
52 |
53 | public extension FormatStyle where Self == VectorFormatStyle, FloatingPointFormatStyle> {
54 | static var vector: Self {
55 | return Self(scalarStyle: .number)
56 | }
57 | }
58 |
59 | public extension FormatStyle where Self == VectorFormatStyle, FloatingPointFormatStyle> {
60 | static var vector: Self {
61 | return Self(scalarStyle: .number)
62 | }
63 | }
64 |
65 | public extension FormatStyle where Self == VectorFormatStyle, FloatingPointFormatStyle> {
66 | static var vector: Self {
67 | return Self(scalarStyle: .number)
68 | }
69 | }
70 |
71 | // MARK: -
72 |
73 | public extension FormatStyle where Self == VectorFormatStyle, FloatingPointFormatStyle> {
74 | static var vector: Self {
75 | return Self(scalarStyle: .number)
76 | }
77 | }
78 |
79 | public extension FormatStyle where Self == VectorFormatStyle, FloatingPointFormatStyle> {
80 | static var vector: Self {
81 | return Self(scalarStyle: .number)
82 | }
83 | }
84 |
85 | public extension FormatStyle where Self == VectorFormatStyle, FloatingPointFormatStyle> {
86 | static var vector: Self {
87 | return Self(scalarStyle: .number)
88 | }
89 | }
90 |
91 | public extension SIMD {
92 | func formatted(_ format: S) -> S.FormatOutput where Self == S.FormatInput, S: FormatStyle {
93 | return format.format(self)
94 | }
95 | }
96 |
97 | // TODO: Cannot appease the generic gods.
98 | //public extension SIMD where Scalar: BinaryFloatingPoint {
99 | // func formatted() -> String {
100 | // return formatted(.simd())
101 | // }
102 | //}
103 |
104 | // MARK: -
105 |
106 | extension VectorFormatStyle: ParseableFormatStyle where ScalarStyle: ParseableFormatStyle {
107 | public var parseStrategy: VectorParseStrategy {
108 | return VectorParseStrategy(scalarStrategy: scalarStyle.parseStrategy, compositeStyle: compositeStyle)
109 | }
110 | }
111 |
112 | public struct VectorParseStrategy : ParseStrategy where V: SIMD, ScalarStrategy: ParseStrategy, ScalarStrategy.ParseInput == String, ScalarStrategy.ParseOutput == V.Scalar {
113 |
114 | public var scalarStrategy: ScalarStrategy
115 | public var compositeStyle: CompositeStyle
116 |
117 | public init(scalarStrategy: ScalarStrategy, compositeStyle: CompositeStyle = .mapping) {
118 | self.scalarStrategy = scalarStrategy
119 | self.compositeStyle = compositeStyle
120 | }
121 |
122 | public init(type: V.Type, scalarStrategy: ScalarStrategy, compositeStyle: CompositeStyle = .mapping) {
123 | self.init(scalarStrategy: scalarStrategy, compositeStyle: compositeStyle)
124 | }
125 |
126 | public func parse(_ value: String) throws -> V {
127 | switch compositeStyle {
128 | case .list:
129 | let strategy = SimpleListParseStrategy(substrategy: scalarStrategy)
130 | let scalars = try strategy.parse(value)
131 | return V(scalars)
132 | case .mapping:
133 | let strategy = MappingParseStrategy(keyStrategy: IdentityParseStategy(), valueStrategy: scalarStrategy)
134 | let dictionary = Dictionary(uniqueKeysWithValues: try strategy.parse(value))
135 | switch V.scalarCount {
136 | case 2:
137 | guard let x = dictionary["x"], let y = dictionary["y"] else {
138 | throw SwiftFormatsError.missingKeys
139 | }
140 | return V([x, y])
141 | case 3:
142 | guard let x = dictionary["x"], let y = dictionary["y"], let z = dictionary["z"] else {
143 | throw SwiftFormatsError.missingKeys
144 | }
145 | return V([x, y, z])
146 | case 4:
147 | guard let x = dictionary["x"], let y = dictionary["y"], let z = dictionary["z"], let w = dictionary["w"] else {
148 | throw SwiftFormatsError.missingKeys
149 | }
150 | return V([x, y, z, w])
151 | default:
152 | throw SwiftFormatsError.missingKeys
153 | }
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/MappingFormatStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct MappingFormatStyle : FormatStyle where KeyStyle: FormatStyle, KeyStyle.FormatInput == Key, KeyStyle.FormatOutput == String, ValueStyle: FormatStyle, ValueStyle.FormatInput == Value, ValueStyle.FormatOutput == String {
4 |
5 | let listStyle: SimpleListFormatStyle<(Key, Value), TupleFormatStyle>
6 | let keyValueSeparator: String
7 | let itemSeparator: String
8 |
9 | public init(keyStyle: KeyStyle, valueStyle: ValueStyle, keyValueSeparator: String = ": ", itemSeparator: String = ", ") {
10 | self.keyValueSeparator = keyValueSeparator
11 | self.itemSeparator = itemSeparator
12 | let keyValueStyle = TupleFormatStyle(type: (Key, Value).self, separator: keyValueSeparator, substyle0: keyStyle, substyle1: valueStyle)
13 | self.listStyle = SimpleListFormatStyle(substyle: keyValueStyle, separator: itemSeparator)
14 | }
15 |
16 | public func format(_ value: [(Key, Value)]) -> String {
17 | return listStyle.format(Array(value))
18 | }
19 | }
20 |
21 | // MARK: -
22 |
23 | public extension MappingFormatStyle {
24 | init(keyType: Key.Type, valueType: Value.Type, keyStyle: KeyStyle, valueStyle: ValueStyle, keyValueSeparator: String = ": ", itemSeparator: String = ", ") {
25 | self.init(keyStyle: keyStyle, valueStyle: valueStyle, keyValueSeparator: keyValueSeparator, itemSeparator: itemSeparator)
26 | }
27 |
28 | }
29 |
30 | public extension MappingFormatStyle where Key == String, KeyStyle == IdentityFormatStyle {
31 | init(valueStyle: ValueStyle, keyValueSeparator: String = ": ", itemSeparator: String = ", ") {
32 | self = .init(keyStyle: IdentityFormatStyle(), valueStyle: valueStyle, keyValueSeparator: keyValueSeparator)
33 | }
34 | }
35 |
36 | extension MappingFormatStyle: ParseableFormatStyle where KeyStyle: ParseableFormatStyle, ValueStyle: ParseableFormatStyle {
37 | public var parseStrategy: MappingParseStrategy {
38 | return MappingParseStrategy(listStrategy: listStyle.parseStrategy)
39 | }
40 | }
41 |
42 | // MARK: -
43 |
44 | public struct MappingParseStrategy : ParseStrategy where KeyStrategy: ParseStrategy, KeyStrategy.ParseInput == String, KeyStrategy.ParseOutput == Key, ValueStrategy: ParseStrategy, ValueStrategy.ParseInput == String, ValueStrategy.ParseOutput == Value {
45 | public typealias ListStrategy = SimpleListParseStrategy<(Key, Value), TupleParseStrategy>
46 | let listStrategy: ListStrategy
47 |
48 | public init(listStrategy: ListStrategy) {
49 | self.listStrategy = listStrategy
50 | }
51 |
52 | public func parse(_ value: String) throws -> [(Key, Value)] {
53 | return try listStrategy.parse(value)
54 | }
55 | }
56 |
57 | extension MappingParseStrategy {
58 | public init(keyStrategy: KeyStrategy, valueStrategy: ValueStrategy, keyValueSeparator: String = ":", itemSeparator: String = ",") {
59 | let keyValueStrategy = TupleParseStrategy(type: (Key, Value).self, separators: [keyValueSeparator], substrategy0: keyStrategy, substrategy1: valueStrategy)
60 | self.listStrategy = ListStrategy(substrategy: keyValueStrategy)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/ParseableFormatStyle+Measurement.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct UnitAngleParseStrategy {
4 |
5 | /// The format style's locale and (optionally) the unit's width width to be used to match the candidate unit.
6 | let format: Measurement.FormatStyle
7 | /// Determines how strict the unit parsing detection will be.
8 | /// If `False`: The candidate unit will be case-sensitive matched against the format's `Measurement.FormatStyle.UnitWidth` value.
9 | /// if `True`: The candidate unit will be case-insensitive matched against all possible `Measurement.FormatStyle.UnitWidth` values.
10 | let lenient: Bool
11 |
12 | init(format: Measurement.FormatStyle, lenient: Bool = false) {
13 | self.format = format
14 | self.lenient = lenient
15 | }
16 |
17 | public func lenient(_ isLenient: Bool) -> UnitAngleParseStrategy {
18 | Self(format: format, lenient: isLenient)
19 | }
20 | }
21 |
22 | // MARK: - ParseStrategy
23 |
24 | extension UnitAngleParseStrategy: ParseStrategy {
25 | public func parse(_ value: String) throws -> Measurement {
26 | let parsedUnit = try parseUnit(from: value, for: format.locale)
27 | let numericalValue = try Double(value, format: format.numberFormatStyle ?? .number)
28 | return Measurement(value: numericalValue, unit: parsedUnit)
29 | }
30 | }
31 |
32 | // MARK: - Private Methods
33 |
34 | private extension UnitAngleParseStrategy {
35 | private func parseUnit(from string: String, for locale: Locale) throws -> UnitAngle {
36 | let matchingUnit = try UnitAngle.allFoundationUnits.first { candidateUnit in
37 | let checkWidths = lenient ? [format.width] : Measurement.FormatStyle.UnitWidth.allCases
38 | let unitStrings = try candidateUnit.localizedUnitStrings(for: locale, widths: checkWidths)
39 | switch lenient {
40 | case true:
41 | return unitStrings.map({ $0.lowercased() }).contains(where: { string.contains($0.lowercased()) })
42 | case false:
43 | return unitStrings.contains(where: { string.contains($0) })
44 | }
45 |
46 | }
47 | guard let matchingUnit else {
48 | throw SwiftFormatsError.unitCannotBeDetermined
49 | }
50 | return matchingUnit
51 | }
52 | }
53 |
54 | // MARK: - Convenience Initializers
55 |
56 | extension Measurement.FormatStyle: ParseableFormatStyle where UnitType == UnitAngle {
57 | public var parseStrategy: UnitAngleParseStrategy {
58 | UnitAngleParseStrategy(format: self, lenient: false)
59 | }
60 | }
61 |
62 | public extension Measurement where UnitType == UnitAngle {
63 | init(_ value: String, format: Measurement.FormatStyle, lenient: Bool = false) throws {
64 | self = try format.parseStrategy.lenient(lenient).parse(value)
65 | }
66 |
67 | init(_ value: String, locale: Locale = .autoupdatingCurrent) throws {
68 | self = try Measurement.FormatStyle.measurement(width: .wide).locale(locale).parseStrategy.parse(value)
69 | }
70 | }
71 |
72 | // MARK: - UnitAngle Extensions
73 |
74 | public extension UnitAngle {
75 | static var allFoundationUnits: [UnitAngle] {
76 | [
77 | .degrees,
78 | .radians,
79 | .arcMinutes,
80 | .arcSeconds,
81 | .gradians,
82 | .revolutions,
83 | ]
84 | }
85 | }
86 |
87 | extension UnitAngle {
88 |
89 | private static var whitespaceNewlinesAndCharacterForZero: CharacterSet {
90 | var set = CharacterSet()
91 | set.insert(charactersIn: "0")
92 | set.formUnion(.whitespacesAndNewlines)
93 | return set
94 | }
95 |
96 | func localizedUnitString(for locale: Locale, width: Measurement.FormatStyle.UnitWidth) throws -> String {
97 | // Create a zero value measurement to feed into the Measurement.FormatStyle
98 | let dummyMeasurement = Measurement(value: 0, unit: self)
99 | let formattedString = dummyMeasurement.formatted(.measurement(width: width).locale(locale))
100 |
101 | // Remove only whitespace characters and the "0" character from the string. Leaving only the localized unit
102 | return String(formattedString.components(separatedBy: Self.whitespaceNewlinesAndCharacterForZero).joined())
103 | }
104 |
105 | func localizedUnitStrings(
106 | for locale: Locale,
107 | widths: [Measurement.FormatStyle.UnitWidth]
108 | ) throws -> [String] {
109 | try widths.map { try localizedUnitString(for: locale, width: $0) }
110 | }
111 | }
112 |
113 | // MARK: - Measurement Extensions
114 |
115 | extension Measurement.FormatStyle.UnitWidth: CaseIterable {
116 | public static var allCases: [Self] {
117 | [
118 | .wide,
119 | .abbreviated,
120 | .narrow
121 | ]
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/RadixedIntegerFormatStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Format integers with a given radix, prefix, padding, grouping and case.
4 | public struct RadixedIntegerFormatStyle: FormatStyle, Hashable, Codable where FormatInput: BinaryInteger {
5 | public typealias FormatInput = FormatInput
6 | public typealias FormatOutput = String
7 |
8 | private var radix: Int
9 | private var prefix: Prefix
10 | private var width: Width
11 | private var padding: String
12 | private var groupCount: Int?
13 | private var groupSeparator: String
14 | private var uppercase: Bool
15 |
16 | public enum Prefix: Hashable, Codable {
17 | case none
18 | case standard
19 | case custom(String)
20 | }
21 |
22 | public enum Width: Hashable, Codable {
23 | case minimum
24 | case byType
25 | case count(Int)
26 | }
27 |
28 | public init(radix: Int = 10, prefix: Prefix = .none, width: Width, padding: Character = "0", groupCount: Int? = nil, groupSeparator: String = "_", uppercase: Bool = false) {
29 | self.radix = radix
30 | self.prefix = prefix
31 | self.width = width
32 | self.padding = String(padding)
33 | self.groupCount = groupCount
34 | self.groupSeparator = groupSeparator
35 | self.uppercase = uppercase
36 | }
37 |
38 | // swiftlint:disable:next cyclomatic_complexity function_body_length
39 | public func format(_ value: FormatInput) -> String {
40 | var digits = String(value, radix: radix, uppercase: uppercase)
41 |
42 | switch width {
43 | case .minimum:
44 | break
45 | case .byType:
46 | let bitsPerCharacter: Int
47 | switch radix {
48 | case 2:
49 | bitsPerCharacter = 1
50 | case 8:
51 | bitsPerCharacter = 3
52 | case 16:
53 | bitsPerCharacter = 4
54 | default:
55 | fatalError("Radix must be 2, 8 or 16. Not \(radix)")
56 | }
57 | if bitsPerCharacter != 0 {
58 | let maxDigits = MemoryLayout.size * 8 / bitsPerCharacter
59 | let leadingPaddingCount = maxDigits - digits.count
60 | if leadingPaddingCount > 0 {
61 | let padding = String(repeating: padding, count: leadingPaddingCount)
62 | digits.insert(contentsOf: padding, at: digits.startIndex)
63 | }
64 | }
65 | case .count(let count):
66 | let leadingPaddingCount = count - digits.count
67 | if leadingPaddingCount > 0 {
68 | let padding = String(repeating: padding, count: leadingPaddingCount)
69 | digits.insert(contentsOf: padding, at: digits.startIndex)
70 | }
71 | digits = String(digits.suffix(count))
72 | }
73 |
74 | if let groupCount {
75 | digits = digits.chunks(ofCount: groupCount).joined(separator: groupSeparator)
76 | }
77 |
78 | switch (prefix, radix) {
79 | case (.none, _):
80 | break
81 | case (.standard, 2):
82 | digits = "0b" + digits
83 | case (.standard, 8):
84 | digits = "0o" + digits
85 | case (.standard, 16):
86 | digits = "0x" + digits
87 | case (.custom(let prefix), _):
88 | digits = prefix + digits
89 | default:
90 | // fatalError("No standard prefix for radix \(radix)")
91 | break
92 | }
93 |
94 | return digits
95 | }
96 | }
97 |
98 | public extension RadixedIntegerFormatStyle {
99 | init(radix: Int = 10, prefix: Prefix = .none, leadingZeros: Bool = false, groupCount: Int? = nil, groupSeparator: String = "_", uppercase: Bool = false) {
100 | self = .init(radix: radix, prefix: prefix, width: leadingZeros ? .byType : .minimum, padding: "0", groupCount: groupCount, groupSeparator: groupSeparator, uppercase: uppercase)
101 | }
102 | }
103 |
104 | // MARK: -
105 |
106 | public extension RadixedIntegerFormatStyle {
107 | // TODO: Add more styling functions here
108 | func group(_ count: Int, separator: String = "_") -> Self {
109 | var copy = self
110 | copy.groupCount = count
111 | copy.groupSeparator = separator
112 | return copy
113 | }
114 |
115 | func leadingZeros(_ leadingZeros: Bool = true) -> Self {
116 | var copy = self
117 | copy.width = leadingZeros ? .byType : .minimum
118 | copy.padding = "0"
119 | return copy
120 | }
121 |
122 | func prefix(_ prefix: Prefix) -> Self {
123 | var copy = self
124 | copy.prefix = prefix
125 | return copy
126 | }
127 | }
128 |
129 | // MARK: -
130 |
131 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
132 | static var hex: Self {
133 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
134 | }
135 |
136 | static var binary: Self {
137 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
138 | }
139 |
140 | static var octal: Self {
141 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
142 | }
143 | }
144 |
145 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
146 | static var hex: Self {
147 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
148 | }
149 |
150 | static var binary: Self {
151 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
152 | }
153 |
154 | static var octal: Self {
155 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
156 | }
157 | }
158 |
159 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
160 | static var hex: Self {
161 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
162 | }
163 |
164 | static var binary: Self {
165 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
166 | }
167 |
168 | static var octal: Self {
169 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
170 | }
171 | }
172 |
173 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
174 | static var hex: Self {
175 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
176 | }
177 |
178 | static var binary: Self {
179 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
180 | }
181 |
182 | static var octal: Self {
183 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
184 | }
185 | }
186 |
187 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
188 | static var hex: Self {
189 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
190 | }
191 |
192 | static var binary: Self {
193 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
194 | }
195 |
196 | static var octal: Self {
197 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
198 | }
199 | }
200 |
201 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
202 | static var hex: Self {
203 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
204 | }
205 |
206 | static var binary: Self {
207 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
208 | }
209 |
210 | static var octal: Self {
211 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
212 | }
213 | }
214 |
215 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
216 | static var hex: Self {
217 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
218 | }
219 |
220 | static var binary: Self {
221 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
222 | }
223 |
224 | static var octal: Self {
225 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
226 | }
227 | }
228 |
229 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
230 | static var hex: Self {
231 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
232 | }
233 |
234 | static var binary: Self {
235 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
236 | }
237 |
238 | static var octal: Self {
239 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
240 | }
241 | }
242 |
243 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
244 | static var hex: Self {
245 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
246 | }
247 |
248 | static var binary: Self {
249 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
250 | }
251 |
252 | static var octal: Self {
253 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
254 | }
255 | }
256 |
257 | public extension FormatStyle where Self == RadixedIntegerFormatStyle {
258 | static var hex: Self {
259 | Self(radix: 16, prefix: .standard, leadingZeros: false, groupCount: nil, uppercase: true)
260 | }
261 |
262 | static var binary: Self {
263 | Self(radix: 2, prefix: .standard, leadingZeros: false, groupCount: nil)
264 | }
265 |
266 | static var octal: Self {
267 | Self(radix: 8, prefix: .standard, leadingZeros: false, groupCount: nil)
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/Scratch.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct IdentityFormatStyle : FormatStyle {
4 | public init() {
5 | }
6 |
7 | public func format(_ value: Value) -> Value {
8 | return value
9 | }
10 | }
11 |
12 | extension IdentityFormatStyle: ParseableFormatStyle {
13 | public var parseStrategy: IdentityParseStategy {
14 | return IdentityParseStategy()
15 | }
16 | }
17 |
18 | public struct IdentityParseStategy : ParseStrategy {
19 | public init() {
20 | }
21 | public func parse(_ value: Value) throws -> Value {
22 | return value
23 | }
24 | }
25 |
26 | // TODO: Again annoyed that FormatStyle has to be Hashable/Codable. Also need to do a AnyParseableFormatStyle
27 | internal struct AnyFormatStyle : FormatStyle {
28 | var closure: (FormatInput) -> FormatOutput
29 |
30 | init(_ closure: @escaping (FormatInput) -> FormatOutput) {
31 | self.closure = closure
32 | }
33 |
34 | func format(_ value: FormatInput) -> FormatOutput {
35 | return closure(value)
36 | }
37 |
38 | // MARK: -
39 |
40 | // swiftlint:disable:next unavailable_function
41 | static func == (lhs: Self, rhs: Self) -> Bool {
42 | fatalError("Unimplemented")
43 | }
44 |
45 | // swiftlint:disable:next unavailable_function
46 | func hash(into hasher: inout Hasher) {
47 | fatalError("Unimplemented")
48 | }
49 |
50 | // swiftlint:disable:next unavailable_function
51 | init(from decoder: Decoder) throws {
52 | fatalError("Unimplemented")
53 | }
54 |
55 | // swiftlint:disable:next unavailable_function
56 | func encode(to encoder: Encoder) throws {
57 | fatalError("Unimplemented")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/SwiftFormats/SimpleListFormatStyle.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // TODO: This could work with any Sequence and only the ParseableFormatStyle needs to restrict to an Array
4 | /// A format style that formats a list of elements by formatting each element with a substyle and joining them with a separator. See also `ListFormatStyle`
5 | public struct SimpleListFormatStyle : FormatStyle where Substyle: FormatStyle, Element == Substyle.FormatInput, Substyle.FormatOutput == String {
6 |
7 | public var substyle: Substyle
8 | public var separator: String
9 | public var prefix: String?
10 | public var suffix: String?
11 |
12 | public init(substyle: Substyle, separator: String = ", ", prefix: String? = nil, suffix: String? = nil) {
13 | self.substyle = substyle
14 | self.separator = separator
15 | self.prefix = prefix
16 | self.suffix = suffix
17 | }
18 |
19 | public func format(_ value: [Element]) -> String {
20 | return (prefix ?? "") + value.map { substyle.format($0) }.joined(separator: separator) + (suffix ?? "")
21 | }
22 | }
23 |
24 | extension SimpleListFormatStyle: ParseableFormatStyle where Substyle: ParseableFormatStyle {
25 | public var parseStrategy: SimpleListParseStrategy {
26 | SimpleListParseStrategy(substrategy: substyle.parseStrategy)
27 | }
28 | }
29 |
30 | // MARK: -
31 |
32 | /// A parse strategy that parses a list of elements by parsing each element with a substrategy and splitting them by a separator.
33 | public struct SimpleListParseStrategy : ParseStrategy where Substrategy: ParseStrategy, Element == Substrategy.ParseOutput, Substrategy.ParseInput == String {
34 |
35 | // TODO: allow skipping, and just split by whitespace
36 |
37 | public private(set) var substrategy: Substrategy
38 | public var separator: String
39 | // public var prefix: String?
40 | // public var suffix: String?
41 | public var countRange: ClosedRange = .zero ... .max
42 |
43 | public init(substrategy: Substrategy, separator: String = ",", countRange: ClosedRange = .zero ... .max) {
44 | self.substrategy = substrategy
45 | self.separator = separator
46 | self.countRange = countRange
47 | }
48 |
49 | /// TODO: this will totally break when the substrategy emits commas (e.g. localisation that use commas as digit group separators)
50 | public func parse(_ value: String) throws -> [Element] {
51 | let components = try value
52 | .split(separator: separator, omittingEmptySubsequences: false)
53 | .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
54 | .map { try substrategy.parse(String($0)) }
55 |
56 | guard countRange.contains(components.count) else {
57 | throw SwiftFormatsError.countError
58 | }
59 | return components
60 | }
61 | }
62 |
63 | extension SimpleListParseStrategy: IncrementalParseStrategy {
64 | public func incrementalParse(_ value: inout String) throws -> [Element] {
65 | var elements: [Element] = []
66 | let scanner = Scanner(string: value)
67 | scanner.charactersToBeSkipped = nil
68 | while !scanner.isAtEnd && elements.count < countRange.upperBound {
69 | if let chunk = scanner.scanUpToString(separator) {
70 | elements.append(try substrategy.parse(chunk))
71 | }
72 | _ = scanner.scanString(separator)
73 | }
74 | guard countRange.contains(elements.count) else {
75 | throw SwiftFormatsError.countError
76 | }
77 | value = String(value[scanner.currentIndex ..< value.endIndex])
78 | return elements
79 | }
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/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