├── .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(_ value: Double, unit: Unit, format: Style) where Style: FormatStyle, Style.FormatInput == Measurement, Style.FormatOutput == String, Unit: Dimension { 32 | let measurement = Measurement(value: value, unit: unit) 33 | appendInterpolation(format.format(measurement)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftFormats/Support.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import simd 3 | import RegexBuilder 4 | 5 | public enum SwiftFormatsError: Error { 6 | case parseError 7 | case unitCannotBeDetermined 8 | case missingKeys 9 | case countError 10 | } 11 | 12 | internal func unimplemented(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Never { 13 | fatalError(message(), file: file, line: line) 14 | } 15 | 16 | internal func degreesToRadians(_ value: F) -> F where F: FloatingPoint { 17 | value * .pi / 180 18 | } 19 | 20 | internal func radiansToDegrees(_ value: F) -> F where F: FloatingPoint { 21 | value * 180 / .pi 22 | } 23 | 24 | // MARK: - 25 | 26 | internal extension SIMD { 27 | var scalars: [Scalar] { 28 | (0 ..< scalarCount).map { self[$0] } 29 | } 30 | } 31 | 32 | 33 | /// Generates a ChoiceOf regex pattern from an array of strings. 34 | extension Array: RegexComponent where Element == String { 35 | public var regex: Regex { 36 | 37 | guard let first else { 38 | fatalError("Cannot create ChoiceOf with zero elements.") 39 | } 40 | 41 | return Regex { 42 | dropFirst().reduce(AlternationBuilder.buildPartialBlock(first: first)) { regex, element in 43 | return AlternationBuilder.buildPartialBlock(accumulated: regex, next: element) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftFormats/TupleFormatStyle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RegexBuilder 3 | 4 | public struct TupleFormatStyle : FormatStyle where Substyle0: FormatStyle, Substyle1: FormatStyle, Element0 == Substyle0.FormatInput, Substyle0.FormatOutput == String, Element1 == Substyle1.FormatInput, Substyle1.FormatOutput == String { 5 | public typealias FormatInput = (Element0, Element1) 6 | public typealias FormatOutput = String 7 | 8 | var separator: String 9 | var substyle0: Substyle0 10 | var substyle1: Substyle1 11 | 12 | public init(type: (Element0, Element1).Type, separator: String, substyle0: Substyle0, substyle1: Substyle1) { 13 | self.separator = separator 14 | self.substyle0 = substyle0 15 | self.substyle1 = substyle1 16 | } 17 | 18 | public func format(_ value: (Element0, Element1)) -> String { 19 | return substyle0.format(value.0) + separator + substyle1.format(value.1) 20 | } 21 | } 22 | 23 | extension TupleFormatStyle: ParseableFormatStyle where Substyle0: ParseableFormatStyle, Substyle1: ParseableFormatStyle { 24 | public var parseStrategy: TupleParseStrategy { 25 | TupleParseStrategy(type: (Element0, Element1).self, separators: [separator.trimmingCharacters(in: .whitespaces)], substrategy0: substyle0.parseStrategy, substrategy1: substyle1.parseStrategy) 26 | } 27 | } 28 | 29 | // MARK: - 30 | 31 | public struct TupleParseStrategy : ParseStrategy where Substrategy0: ParseStrategy, Substrategy0.ParseInput == String, Substrategy0.ParseOutput == Element0, Substrategy1: ParseStrategy, Substrategy1.ParseInput == String, Substrategy1.ParseOutput == Element1 { 32 | 33 | public typealias ParseInput = String 34 | public typealias ParseOutput = (Element0, Element1) 35 | 36 | var separators: [String] 37 | var disallowWhitespace: Bool 38 | var substrategy0: Substrategy0 39 | var substrategy1: Substrategy1 40 | 41 | public init(type: (Element0, Element1).Type, separators: [String], disallowWhitespace: Bool = false, substrategy0: Substrategy0, substrategy1: Substrategy1) { 42 | self.separators = separators 43 | self.disallowWhitespace = disallowWhitespace 44 | self.substrategy0 = substrategy0 45 | self.substrategy1 = substrategy1 46 | } 47 | 48 | // swiftlint:disable:next function_body_length 49 | public func parse(_ value: String) throws -> (Element0, Element1) { 50 | let string0: String 51 | let string1: String 52 | if disallowWhitespace { 53 | let regex = Regex { 54 | #/^/# 55 | Capture { 56 | ZeroOrMore { 57 | .any 58 | } 59 | One(.horizontalWhitespace.inverted) 60 | } 61 | ChoiceOf { 62 | separators 63 | } 64 | Capture { 65 | One(.horizontalWhitespace.inverted) 66 | ZeroOrMore { 67 | .any 68 | } 69 | } 70 | #/$/# 71 | } 72 | guard let match = value.firstMatch(of: regex) else { 73 | throw SwiftFormatsError.parseError 74 | } 75 | string0 = String(match.output.1) 76 | string1 = String(match.output.2) 77 | } 78 | else { 79 | let regex = Regex { 80 | #/^/# 81 | Capture { 82 | OneOrMore(.reluctant) { 83 | .any 84 | } 85 | } 86 | ZeroOrMore(.horizontalWhitespace) 87 | ChoiceOf { 88 | separators 89 | } 90 | ZeroOrMore(.horizontalWhitespace) 91 | Capture { 92 | OneOrMore(.reluctant) { 93 | .any 94 | } 95 | } 96 | #/$/# 97 | } 98 | 99 | guard let match = value.firstMatch(of: regex) else { 100 | throw SwiftFormatsError.parseError 101 | } 102 | string0 = String(match.output.1) 103 | string1 = String(match.output.2) 104 | } 105 | let element0 = try substrategy0.parse(string0) 106 | let element1 = try substrategy1.parse(string1) 107 | return (element0, element1) 108 | } 109 | } 110 | 111 | // MARK: - 112 | 113 | public extension TupleParseStrategy { 114 | func separators(_ separators: [String]) -> Self { 115 | var copy = self 116 | copy.separators = separators 117 | return copy 118 | } 119 | 120 | func disallowingWhitespace() -> Self { 121 | var copy = self 122 | copy.disallowWhitespace = true 123 | return copy 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /TestPlans/SwiftFormats.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "3A4C2ACE-27CE-4F8C-B93B-0EC578912072", 5 | "name" : "Test Scheme Action", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:", 18 | "identifier" : "SwiftFormatsTests", 19 | "name" : "SwiftFormatsTests" 20 | } 21 | } 22 | ], 23 | "version" : 1 24 | } 25 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/AngleTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftFormats 3 | import SwiftUI 4 | import XCTest 5 | 6 | class AngleValueTests: XCTestCase { 7 | func test1() { 8 | let angle = Angle(degrees: 90) 9 | XCTAssertEqual(angle.formatted(), "90°") 10 | XCTAssertEqual("\(angle, format: .angle)", "90°") 11 | XCTAssertEqual("\(angle, format: .angle.degrees)", "90°") 12 | XCTAssertEqual("\(angle, format: .angle.radians)", "1.570796rad") 13 | 14 | XCTAssertEqual(try AngleValueParseStrategy().parse("90°"), Angle(degrees: 90)) 15 | XCTAssertEqual(try AngleValueParseStrategy(defaultInputUnit: .degrees).parse("90"), Angle(degrees: 90)) 16 | XCTAssertEqual(try AngleValueParseStrategy().parse("1.570796rad").degrees, 90, accuracy: 0.001) 17 | XCTAssertEqual(try AngleValueParseStrategy().parse("90"), Angle(degrees: 90)) 18 | XCTAssertThrowsError(try AngleValueParseStrategy(defaultInputUnit: nil).parse("90")) 19 | XCTAssertThrowsError(try AngleValueParseStrategy().parse("xxx°")) 20 | 21 | XCTAssertEqual(try AngleValueParseStrategy().parse(" 90°"), Angle(degrees: 90)) 22 | XCTAssertEqual(try AngleValueParseStrategy().parse("90° "), Angle(degrees: 90)) 23 | XCTAssertEqual(try AngleValueParseStrategy().parse("90 °"), Angle(degrees: 90)) 24 | XCTAssertEqual(try AngleValueParseStrategy().parse(" 90 ° "), Angle(degrees: 90)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/BoolTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftFormats 3 | import XCTest 4 | 5 | class BoolValueTests: XCTestCase { 6 | func testFormatting() { 7 | XCTAssertEqual(true.formatted(), "true") 8 | XCTAssertEqual(false.formatted(), "false") 9 | XCTAssertEqual(true.formatted(.bool), "true") 10 | XCTAssertEqual(false.formatted(.bool), "false") 11 | XCTAssertEqual(true.formatted(.bool.true("YES")), "YES") 12 | XCTAssertEqual(false.formatted(.bool.false("NO")), "NO") 13 | } 14 | 15 | func testParsing() { 16 | XCTAssertEqual(try BoolParseStrategy().parse("true"), true) 17 | XCTAssertEqual(try BoolParseStrategy().parse("false"), false) 18 | XCTAssertThrowsError(try BoolParseStrategy().parse("aardvark")) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/CoreGraphicsTests.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import SwiftFormats 3 | import XCTest 4 | 5 | class CoreGraphicsTests: XCTestCase { 6 | func testRectangle() { 7 | XCTAssertEqual(CGRect(x: 0, y: 0, width: 0, height: 0).formatted(), "x: 0, y: 0, width: 0, height: 0") 8 | XCTAssertEqual(CGRect(x: 1, y: 2, width: 3, height: 4).formatted(), "x: 1, y: 2, width: 3, height: 4") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/MappingTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import SwiftFormats 3 | import XCTest 4 | 5 | class MappingTests: XCTestCase { 6 | func test1() { 7 | let style = MappingFormatStyle(keyType: Int.self, valueType: Int.self, keyStyle: .number, valueStyle: .number) 8 | XCTAssertEqual(style.format([(1, 10), (2, 20)]), "1: 10, 2: 20") 9 | let parser = style.parseStrategy 10 | XCTAssertEqual(Dictionary(uniqueKeysWithValues: try parser.parse("1:10, 2:20")), [1: 10, 2: 20]) 11 | } 12 | 13 | func test2() { 14 | let style = MappingFormatStyle(keyType: String.self, valueType: Int.self, keyStyle: IdentityFormatStyle(), valueStyle: .number) 15 | XCTAssertEqual(style.format([("A", 10), ("B", 20)]), "A: 10, B: 20") 16 | let parser = style.parseStrategy 17 | XCTAssertEqual(Dictionary(uniqueKeysWithValues: try parser.parse("A:10, B:20")), ["A": 10, "B": 20]) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/MatrixTests.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | @testable import SwiftFormats 4 | import XCTest 5 | import simd 6 | 7 | private let locale = Locale(identifier: "en_US") 8 | 9 | class MatrixTests: XCTestCase { 10 | func test1() throws { 11 | let matrix = simd_float4x4(rows: [ 12 | [0, 1, 2, 3], 13 | [4, 5, 6, 7], 14 | [8, 9, 10, 11], 15 | [12, 13, 14, 15], 16 | ]) 17 | let string = "0, 1, 2, 3\n4, 5, 6, 7\n8, 9, 10, 11\n12, 13, 14, 15" 18 | XCTAssertEqual("\(matrix, format: .matrix)", string) 19 | XCTAssertEqual(try MatrixParseStrategy(scalarStrategy: FloatingPointFormatStyle.number.parseStrategy).parse(string), matrix) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/QuaternionTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import SwiftFormats 3 | import XCTest 4 | import simd 5 | 6 | class QuaternionTests: XCTestCase { 7 | func test1() throws { 8 | let angle = 0.785398 // 45° 9 | let q = simd_quatd(angle: angle, axis: [0, 0, 1]) 10 | XCTAssertEqual("\(q, format: .quaternion.numberStyle(.number.precision(.fractionLength(...2))))", "real: 0.92, ix: 0, iy: 0, iz: 0.38") 11 | XCTAssertEqual("\(q, format: .quaternion.style(.components).numberStyle( .number.precision(.fractionLength(...2))))", "real: 0.92, ix: 0, iy: 0, iz: 0.38") 12 | XCTAssertEqual("\(q, format: .quaternion.style(.vector).numberStyle(.number.precision(.fractionLength(...2))))", "x: 0, y: 0, z: 0.38, w: 0.92") 13 | XCTAssertEqual("\(q, format: .quaternion.style(.angleAxis).numberStyle( .number.precision(.fractionLength(...2))))", "angle: 0.79, x: 0, y: 0, z: 1") 14 | } 15 | 16 | func testParsing() throws { 17 | let angle = 0.785398 // 45° 18 | let q = simd_quatd(angle: angle, axis: [0, 0, 1]) 19 | let s = q.formatted() 20 | 21 | let strategy = QuaternionParseStrategy(type: simd_quatd.self) 22 | 23 | let q2 = try strategy.parse(s) 24 | XCTAssertEqual(q2.formatted(), "real: 0.92388, ix: 0, iy: 0, iz: 0.382683") 25 | } 26 | 27 | func testStyles() throws { 28 | let angle = 0.785398 // 45° 29 | let q = simd_quatd(angle: angle, axis: [0, 0, 1]) 30 | var style = QuaternionFormatStyle(type: simd_quatd.self) 31 | XCTAssertEqual(try style.parseStrategy.parse(style.format(q)).formatted(), q.formatted()) 32 | style = style.style(.components) 33 | XCTAssertEqual(try style.style(.components).parseStrategy.parse(style.format(q)).formatted(), q.formatted()) 34 | style = style.style(.vector) 35 | XCTAssertEqual(try style.parseStrategy.parse(style.format(q)).formatted(), q.formatted()) 36 | style = style.style(.angleAxis) 37 | XCTAssertEqual(try style.parseStrategy.parse(style.format(q)).formatted(), q.formatted()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/SimpleListTests.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | @testable import SwiftFormats 4 | import XCTest 5 | 6 | private let locale = Locale(identifier: "en_US") 7 | private let fp = FloatingPointFormatStyle.number 8 | 9 | class SimpleListTests: XCTestCase { 10 | func test1() { 11 | let style = SimpleListFormatStyle(substyle: fp).locale(locale) 12 | XCTAssertEqual(style.format([1.1, 2.2, 3.3, 4.4]), "1.1, 2.2, 3.3, 4.4") 13 | let parser = style.parseStrategy 14 | XCTAssertEqual(try parser.parse("1.1, 2.2, 3.3, 4.4"), [1.1, 2.2, 3.3, 4.4]) 15 | } 16 | 17 | func testIncrementalParsing1() { 18 | let string = "1, 2, 3, 4, 5" 19 | let listStrategy = SimpleListParseStrategy(substrategy: fp.parseStrategy, countRange: 3...3) 20 | XCTAssertThrowsError(try listStrategy.parse(string)) 21 | var copy = string 22 | XCTAssertEqual(try listStrategy.incrementalParse(©), [1, 2, 3]) 23 | XCTAssertEqual(copy, " 4, 5") 24 | } 25 | 26 | func testIncrementalParsing2() { 27 | let string = "1\n2\n3\n4\n5" 28 | let listStrategy = SimpleListParseStrategy(substrategy: fp.parseStrategy, separator: "\n", countRange: 3...3) 29 | XCTAssertThrowsError(try listStrategy.parse(string)) 30 | var copy = string 31 | XCTAssertEqual(try listStrategy.incrementalParse(©), [1, 2, 3]) 32 | XCTAssertEqual(copy, "4\n5") 33 | } 34 | 35 | func testIncrementalParsing3() { 36 | let string = "1,2,3\n4,5" 37 | let innerStrategy = SimpleListParseStrategy(substrategy: fp.parseStrategy, separator: ",") 38 | let outerStrategy = SimpleListParseStrategy(substrategy: innerStrategy, separator: "\n") 39 | XCTAssertEqual(try outerStrategy.parse(string), [[1, 2, 3], [4, 5]]) 40 | } 41 | 42 | func testSpaces() { 43 | let string = " A, B, C " 44 | let strategy = SimpleListParseStrategy(substrategy: IdentityParseStategy(), separator: ",") 45 | XCTAssertEqual(try strategy.parse(string), ["A", "B", "C"]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/SwiftFormatsTests.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | @testable import SwiftFormats 4 | import XCTest 5 | import simd 6 | 7 | private let locale = Locale(identifier: "en_US") 8 | 9 | class StringTests: XCTestCase { 10 | func test1() { 11 | XCTAssertEqual(String(123, format: .described), "123") 12 | XCTAssertEqual(String(123, format: .number), "123") 13 | XCTAssertEqual(String(123, format: .dumped), "- 123\n") // TODO: can dump format change? Probably 14 | } 15 | } 16 | 17 | class FormatStyleTests: XCTestCase { 18 | func testIntegerFormatStyle() { 19 | XCTAssertEqual(RadixedIntegerFormatStyle(radix: 2).format(UInt8.zero), "0") 20 | XCTAssertEqual(RadixedIntegerFormatStyle(radix: 2).format(UInt8.max), "11111111") 21 | XCTAssertEqual(RadixedIntegerFormatStyle(radix: 2).format(UInt8(0b11)), "11") 22 | XCTAssertEqual(RadixedIntegerFormatStyle(radix: 2, width: .count(4)).format(UInt8(0b11)), "0011") 23 | XCTAssertEqual(RadixedIntegerFormatStyle(radix: 2, width: .count(2)).format(UInt8.max), "11") 24 | 25 | XCTAssertEqual("\(255, format: .hex)", "0xFF") 26 | XCTAssertEqual(Int(255).formatted(.hex), "0xFF") 27 | XCTAssertEqual(Int(65535).formatted(.hex.group(2)), "0xFF_FF") 28 | XCTAssertEqual(Int(12345).formatted(.hex.group(2)), "0x30_39") 29 | } 30 | 31 | func testOthers() { 32 | XCTAssertEqual("\(100, format: .described)", "100") 33 | XCTAssertEqual("\("100", format: .described)", "100") 34 | } 35 | } 36 | 37 | class DMSTests: XCTestCase { 38 | func test1() { 39 | XCTAssertEqual("\(45.25125, format: .dmsNotation(mode: .decimalDegrees).locale(locale))", "45.25125°") 40 | XCTAssertEqual("\(45.25125, format: .dmsNotation(mode: .decimalMinutes).locale(locale))", "45° 15.075′") 41 | XCTAssertEqual("\(45.25125, format: .dmsNotation(mode: .decimalSeconds).locale(locale))", "45° 15′ 4.5″") 42 | } 43 | } 44 | 45 | class AngleTests: XCTestCase { 46 | func test1() throws { 47 | XCTAssertEqual(45.25125.formatted(.dmsNotation().locale(locale)), "45.25125°") 48 | XCTAssertEqual("\(45.25125, format: .angle(inputUnit: .degrees, outputUnit: .degrees).locale(locale))", "45.25125°") 49 | XCTAssertEqual("\(45.25125, format: .angle(inputUnit: .degrees, outputUnit: .radians).locale(locale))", "0.789783rad") 50 | XCTAssertEqual("\(0.789783, format: .angle(inputUnit: .radians, outputUnit: .degrees).locale(locale))", "45.251233°") 51 | XCTAssertEqual("\(0.789783, format: .angle(inputUnit: .radians, outputUnit: .radians).locale(locale))", "0.789783rad") 52 | XCTAssertEqual(45.25125.formatted(.dmsNotation().locale(locale)), "45.25125°") 53 | XCTAssertEqual(45.25125.formatted(.angle(inputUnit: .degrees, outputUnit: .radians)), "0.789783rad") 54 | 55 | XCTAssertEqual(try AngleParseStrategy(type: Double.self, defaultInputUnit: .degrees, outputUnit: .degrees).parse("45.25125°"), 45.25125) 56 | XCTAssertEqual(try AngleParseStrategy(type: Double.self, defaultInputUnit: .degrees, outputUnit: .radians).parse("45.25125°"), 0.789783303143084) 57 | XCTAssertEqual(try AngleParseStrategy(type: Double.self, defaultInputUnit: .radians, outputUnit: .radians).parse("0.789783rad"), 0.789783) 58 | XCTAssertEqual(try AngleParseStrategy(type: Double.self, defaultInputUnit: .radians, outputUnit: .degrees).parse("0.789783rad"), 45.2512326311807) 59 | 60 | XCTAssertEqual(try Double("45.25125°", strategy: .angle(defaultInputUnit: .degrees, outputUnit: .degrees)), 45.25125) 61 | XCTAssertEqual(try Double("45.25125°", strategy: .angle(defaultInputUnit: .degrees, outputUnit: .radians)), 0.789783303143084) 62 | XCTAssertEqual(try Double("0.789783 rad", strategy: .angle(defaultInputUnit: .radians, outputUnit: .radians)), 0.789783) 63 | XCTAssertEqual(try Double("0.789783 rad", strategy: .angle(defaultInputUnit: .radians, outputUnit: .degrees)), 45.2512326311807) 64 | 65 | XCTAssertEqual(try Double("0.789783 radian", strategy: .angle(defaultInputUnit: .radians, outputUnit: .radians, locale: Locale(identifier: "fr_FR"))), 0.789783) 66 | XCTAssertEqual(try Double("10", format: .number), 10) 67 | 68 | XCTAssertEqual(try Double("0.789783 radian", format: .angle(inputUnit: .radians, outputUnit: .degrees)), 45.2512326311807) 69 | } 70 | } 71 | 72 | class UnitAngleMeasurementTests: XCTestCase { 73 | 74 | func testParsing() throws { 75 | 76 | let testMeasurement = Measurement(value: 45.0, unit: UnitAngle.degrees) 77 | 78 | let englishUS = Locale(identifier: "en_US") 79 | XCTAssertEqual(try Measurement("45°", format: .measurement(width: .narrow).locale(englishUS)), testMeasurement) 80 | XCTAssertEqual(try Measurement("45 deg", format: .measurement(width: .abbreviated).locale(englishUS)), testMeasurement) 81 | XCTAssertEqual(try Measurement("45 degrees", format: .measurement(width: .wide).locale(englishUS)), testMeasurement) 82 | XCTAssertEqual(try Measurement("45°", locale: englishUS), testMeasurement) 83 | XCTAssertEqual(try Measurement("45 deg", locale: englishUS), testMeasurement) 84 | XCTAssertEqual(try Measurement("45 degrees", locale: englishUS), testMeasurement) 85 | 86 | let frenchFR = Locale(identifier: "fr_FR") 87 | XCTAssertEqual(try Measurement("45°", format: .measurement(width: .narrow).locale(frenchFR)), testMeasurement) 88 | XCTAssertEqual(try Measurement("45°", format: .measurement(width: .abbreviated).locale(frenchFR)), testMeasurement) 89 | XCTAssertEqual(try Measurement("45 degrés", format: .measurement(width: .wide).locale(frenchFR)), testMeasurement) 90 | XCTAssertEqual(try Measurement("45°", locale: frenchFR), testMeasurement) 91 | XCTAssertEqual(try Measurement("45 degrés", locale: frenchFR), testMeasurement) 92 | 93 | let japaneseJP = Locale(identifier: "ja_JP") 94 | XCTAssertEqual(try Measurement("45°", format: .measurement(width: .narrow).locale(japaneseJP)), testMeasurement) 95 | XCTAssertEqual(try Measurement("45度", format: .measurement(width: .abbreviated).locale(japaneseJP)), testMeasurement) 96 | XCTAssertEqual(try Measurement("45度", format: .measurement(width: .wide).locale(japaneseJP)), testMeasurement) 97 | XCTAssertEqual(try Measurement("45°", locale: japaneseJP), testMeasurement) 98 | XCTAssertEqual(try Measurement("45度", locale: japaneseJP), testMeasurement) 99 | 100 | let hindiID = Locale(identifier: "id_HI") 101 | XCTAssertEqual(try Measurement("45°", format: .measurement(width: .narrow).locale(hindiID)), testMeasurement) 102 | XCTAssertEqual(try Measurement("45°", format: .measurement(width: .abbreviated).locale(hindiID)), testMeasurement) 103 | XCTAssertEqual(try Measurement("45 derajat", format: .measurement(width: .wide).locale(hindiID)), testMeasurement) 104 | XCTAssertEqual(try Measurement("45°", locale: hindiID), testMeasurement) 105 | XCTAssertEqual(try Measurement("45 derajat", locale: hindiID), testMeasurement) 106 | } 107 | } 108 | 109 | class CoordinatesTests: XCTestCase { 110 | func test1() { 111 | let coordinate = CLLocationCoordinate2D(latitude: 37.78, longitude: 122.43) 112 | XCTAssertEqual("\(coordinate, format: .coordinates.locale(locale))", "37.78° N, 122.43° E") 113 | } 114 | } 115 | 116 | class HexDumpTests: XCTestCase { 117 | func testHexdump() { 118 | XCTAssertEqual("\(Data([0xDE, 0xED, 0xBE, 0xEF]), format: .hexdump())", "0000000000000000 0xDE 0xED 0xBE 0xEF ????\n") 119 | } 120 | } 121 | 122 | class CGPointTests: XCTestCase { 123 | func test1() { 124 | XCTAssertEqual(CGPoint.zero.formatted(), "0, 0") 125 | XCTAssertEqual(try CGPointParseStrategy().parse("0, 0"), CGPoint.zero) 126 | XCTAssertEqual(try CGPoint("0, 0"), CGPoint.zero) 127 | XCTAssertEqual(try CGPoint("0, 0", format: .point), CGPoint.zero) 128 | XCTAssertEqual(try CGPoint("0, 0", strategy: .point), CGPoint.zero) 129 | XCTAssertEqual(try CGPointParseStrategy().parse("0.1, 2.3"), CGPoint(x: 0.1, y: 2.3)) 130 | XCTAssertThrowsError(try CGPointParseStrategy().parse("")) 131 | XCTAssertThrowsError(try CGPointParseStrategy().parse("0.1")) 132 | XCTAssertThrowsError(try CGPointParseStrategy().parse("1, 2, 2")) 133 | } 134 | } 135 | 136 | class ClosedRangeTests: XCTestCase { 137 | func test1() throws { 138 | XCTAssertEqual("\(1 ... 2, format: ClosedRangeFormatStyle(substyle: .number))", "1 ... 2") 139 | 140 | XCTAssertEqual( 141 | try ClosedRangeFormatStyle(substyle: FloatingPointFormatStyle.number).parseStrategy.parse("1 ... 2"), 142 | 1 ... 2 143 | ) 144 | XCTAssertEqual( 145 | try ClosedRangeFormatStyle(substyle: FloatingPointFormatStyle.number).parseStrategy.parse("1 - 2"), 146 | 1 ... 2 147 | ) 148 | XCTAssertEqual( 149 | try ClosedRangeFormatStyle(substyle: FloatingPointFormatStyle.number).parseStrategy.delimiters([" "]).parse("1 2"), 150 | 1 ... 2 151 | ) 152 | XCTAssertThrowsError( 153 | try ClosedRangeFormatStyle(substyle: FloatingPointFormatStyle.number).parseStrategy.parse("1 2") 154 | ) 155 | } 156 | } 157 | 158 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/TupleTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftFormats 3 | import XCTest 4 | import RegexBuilder 5 | 6 | class TupleTests: XCTestCase { 7 | 8 | func testRegexAssumptions() { 9 | let regex = #/(.*[^\h]),([^\h].*)/# 10 | XCTAssertNotNil("1,2".firstMatch(of: regex)) 11 | XCTAssertNil("1, 2".firstMatch(of: regex)) 12 | 13 | let regex2 = Regex { 14 | ChoiceOf { 15 | [",", ";"] 16 | } 17 | } 18 | XCTAssertNotNil(",".firstMatch(of: regex2)) 19 | XCTAssertNotNil(";".firstMatch(of: regex2)) 20 | XCTAssertNil("x".firstMatch(of: regex2)) 21 | } 22 | 23 | func test1() { 24 | let tuple = (1, 2) 25 | let style = TupleFormatStyle(type: (Int, Int).self, separator: ", ", substyle0: .number, substyle1: .number) 26 | XCTAssertEqual(style.format(tuple), "1, 2") 27 | let strategy = style.parseStrategy 28 | XCTAssertThrowsError(try strategy.parse("1,")) 29 | XCTAssertThrowsError(try strategy.parse(",1")) 30 | XCTAssertNotNil(try strategy.parse("1,2")) 31 | XCTAssertNotNil(try strategy.parse("1 ,2")) 32 | XCTAssertNotNil(try strategy.parse("1, 2")) 33 | XCTAssertNotNil(try strategy.parse("1 , 2")) 34 | XCTAssertEqual(try strategy.parse("1,2").0, 1) 35 | XCTAssertEqual(try strategy.parse("1,2").1, 2) 36 | XCTAssertThrowsError(try strategy.disallowingWhitespace().parse("1, 2")) 37 | XCTAssertEqual(try strategy.disallowingWhitespace().parse("1,2").0, 1) 38 | XCTAssertNotNil(try strategy.separators([";", ","]).parse("1;2")) 39 | XCTAssertNotNil(try strategy.separators([";", ","]).parse("1,2")) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftFormatsTests/VectorTests.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | @testable import SwiftFormats 4 | import XCTest 5 | import simd 6 | 7 | private let locale = Locale(identifier: "en_US") 8 | 9 | class VectorTests: XCTestCase { 10 | func test1() throws { 11 | let vector = SIMD3(0, 1, 2) 12 | // XCTAssertEqual("\(vector, format: .simd())", "x: 0, y: 1, z: 2") 13 | // XCTAssertEqual("\(vector, format: .simd(mappingStyle: false))", "0, 1, 2") 14 | // XCTAssertEqual(try SIMDParseStrategy(scalarStrategy: FloatingPointFormatStyle.number.parseStrategy, mappingStyle: false).parse("0, 1, 2"), vector) 15 | XCTAssertEqual(try VectorParseStrategy(scalarStrategy: FloatingPointFormatStyle.number.parseStrategy, compositeStyle: .mapping).parse("x: 0, y: 1, z: 2"), vector) 16 | } 17 | } 18 | --------------------------------------------------------------------------------