├── .github ├── dependabot.yml └── workflows │ ├── builds.yml │ ├── lint.yml │ ├── nightly.yml │ └── release.yml ├── .gitignore ├── .mise.toml ├── .spi.yml ├── .swiftformat ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── swift-version-compare.xcscheme ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Helper │ ├── Character+Extensions.swift │ ├── String+Regex.swift │ ├── VersionCompareResult.swift │ └── VersionValidationError.swift ├── Resources │ └── PrivacyInfo.xcprivacy ├── SemanticVersionComparable │ ├── BuildMetaData │ │ ├── BuildMetaData+ExpressibleByLiteral.swift │ │ └── BuildMetaData.swift │ ├── PrereleaseIdentifier │ │ ├── PrereleaseIdentifier+Equatable.swift │ │ ├── PrereleaseIdentifier+ExpressibleByLiteral.swift │ │ └── PrereleaseIdentifier.swift │ ├── SemanticVersionComparable+Comparable.swift │ ├── SemanticVersionComparable+Equatable.swift │ ├── SemanticVersionComparable+Hashable.swift │ └── SemanticVersionComparable.swift ├── Version+Bundle.swift ├── Version+OS.swift ├── Version+StringInitializer.swift └── Version.swift └── Tests ├── LinuxMain.swift └── VersionCompareTests ├── SemanticVersionComparableTests.swift ├── VersionTests.swift └── XCTestManifests.swift /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | time: "05:30" 13 | timezone: "Europe/Berlin" 14 | target-branch: "main" 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/builds.yml: -------------------------------------------------------------------------------- 1 | name: builds 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.swift' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | name: Build with Swift ${{ matrix.swift }} on ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ macos-latest ] 19 | swift: ["5.9", "5.10"] 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: maxim-lobanov/setup-xcode@v1 24 | with: 25 | xcode-version: '15.3.0' 26 | - uses: fwal/setup-swift@v2 27 | with: 28 | swift-version: ${{ matrix.swift }} 29 | 30 | - run: swift build -v 31 | - run: swift test -v 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.swift' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | lint: 15 | runs-on: macos-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: jdx/mise-action@v2 19 | - run: swiftlint --strict 20 | - run: swiftformat . --lint --strict -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: nightly-build 2 | 3 | on: 4 | schedule: 5 | - cron: '30 5 * * *' 6 | 7 | jobs: 8 | nightly: 9 | name: Build with Swift ${{ matrix.swift }} on ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest] 13 | swift: ["5.9", "5.10"] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: fwal/setup-swift@v2 18 | with: 19 | swift-version: ${{ matrix.swift }} 20 | 21 | - run: swift build -v 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: macos-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: create release 16 | uses: ncipollo/release-action@v1 17 | with: 18 | draft: true 19 | skipIfReleaseExists: true 20 | bodyFile: "CHANGELOG.md" 21 | 22 | -------------------------------------------------------------------------------- /.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 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 92 | 93 | # vscode 94 | .vscode/ 95 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | swiftlint = "0.55.1" 3 | swiftformat = "0.54.0" -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | swift_version: '5.9' 6 | scheme: swift-version-compare 7 | target: VersionCompare 8 | documentation_targets: [VersionCompare] 9 | - platform: macos 10 | swift_version: '5.9' 11 | scheme: swift-version-compare 12 | target: VersionCompare 13 | documentation_targets: [VersionCompare] 14 | - platform: tvos 15 | swift_version: '5.9' 16 | scheme: swift-version-compare 17 | target: VersionCompare 18 | documentation_targets: [VersionCompare] 19 | - platform: watchos 20 | swift_version: '5.9' 21 | scheme: swift-version-compare 22 | target: VersionCompare 23 | documentation_targets: [VersionCompare] 24 | - platform: visionos 25 | swift_version: '5.9' 26 | scheme: swift-version-compare 27 | target: VersionCompare 28 | documentation_targets: [VersionCompare] 29 | external_links: 30 | documentation: https://mflknr.github.io/swift-version-compare/ 31 | metadata: 32 | authors: 33 | Marius Felkner -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --exclude Tuist,Project.swift 4 | 5 | # format options 6 | 7 | --allman false 8 | --anonymousforeach convert 9 | --binarygrouping 4,8 10 | --commas inline 11 | --comments indent 12 | --decimalgrouping 3,5 13 | --elseposition same-line 14 | --empty void 15 | --exponentcase lowercase 16 | --exponentgrouping disabled 17 | --fractiongrouping disabled 18 | --header ignore 19 | --hexgrouping 4,8 20 | --hexliteralcase uppercase 21 | --ifdef indent 22 | --indent 4 23 | --indentcase false 24 | --importgrouping testable-bottom 25 | --linebreaks lf 26 | --marktypes never 27 | --maxwidth none 28 | --octalgrouping 4,8 29 | --onelineforeach convert 30 | --operatorfunc spaced 31 | --patternlet hoist 32 | --ranges no-space 33 | --self remove 34 | --semicolons inline 35 | --stripunusedargs always 36 | --swiftversion 5.7 37 | --trimwhitespace always 38 | --typeblanklines preserve 39 | --wraparguments preserve 40 | --wrapcollections preserve 41 | 42 | # rules 43 | 44 | --enable blankLinesBetweenImports 45 | --enable blockComments 46 | --enable docComments 47 | --enable isEmpty 48 | --enable markTypes 49 | --enable noExplicitOwnership 50 | --enable wrapConditionalBodies 51 | --enable wrapEnumCases 52 | --enable wrapMultilineConditionalAssignment 53 | --disable preferKeyPath -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Package.swift 3 | - .swiftpm 4 | - .build 5 | 6 | disabled_rules: 7 | - opening_brace 8 | 9 | analyzer_rules: 10 | - capture_variable 11 | #- explicit_self 12 | #- typesafe_array_init 13 | - unused_declaration 14 | - unused_import 15 | 16 | # rules that are commented out are explicitly opted out unless told otherwise 17 | opt_in_rules: 18 | - accessibility_label_for_image 19 | - accessibility_trait_for_button 20 | #- anonymous_argument_in_multiline_closure 21 | #- anyobject_protocol # deprecated 22 | - array_init 23 | - attributes 24 | - balanced_xctest_lifecycle 25 | - closure_end_indentation 26 | - closure_spacing 27 | - collection_alignment 28 | - comma_inheritance 29 | - conditional_returns_on_newline 30 | - contains_over_filter_count 31 | - contains_over_filter_is_empty 32 | - contains_over_first_not_nil 33 | - contains_over_range_nil_comparison 34 | - convenience_type 35 | #- direct_return 36 | - discarded_notification_center_observer 37 | - discouraged_assert 38 | - discouraged_none_name 39 | - discouraged_object_literal 40 | #- discouraged_optional_boolean 41 | #- discouraged_optional_collection 42 | - empty_collection_literal 43 | - empty_count 44 | - empty_string 45 | - empty_xctest_method 46 | - enum_case_associated_values_count 47 | #- explicit_enum_raw_value 48 | - expiring_todo 49 | #- explicit_acl 50 | - explicit_enum_raw_value 51 | - explicit_init 52 | #- explicit_top_level_acl 53 | #- explicit_type_interface 54 | - extension_access_modifier 55 | - fallthrough 56 | - fatal_error_message 57 | - file_header 58 | #- file_name 59 | - file_name_no_space 60 | #- file_types_order 61 | - final_test_case # has been added to the source, but has not been released yet 62 | - first_where 63 | - flatmap_over_map_reduce 64 | - force_unwrapping 65 | #- function_default_parameter_at_end 66 | - ibinspectable_in_extension 67 | #- identical_operands 68 | - implicit_return 69 | - implicitly_unwrapped_optional 70 | #- indentation_width # has conflicts with default xcode settings. use strg+i to indent correctly 71 | #- inert_defer # deprecated 72 | - joined_default_parameter 73 | - last_where 74 | #- legacy_multiple 75 | #- legacy_objc_type # not suitable due to third party libs 76 | - let_var_whitespace 77 | - literal_expression_end_indentation 78 | - local_doc_comment 79 | - lower_acl_than_parent 80 | - missing_docs 81 | - modifier_order 82 | - multiline_arguments 83 | - multiline_arguments_brackets 84 | - multiline_function_chains 85 | - multiline_literal_brackets 86 | - multiline_parameters 87 | - multiline_parameters_brackets 88 | #- nimble_operator 89 | #- no_extension_access_modifier 90 | #- no_grouping_extension 91 | - no_magic_numbers 92 | - non_overridable_class_declaration 93 | - notification_center_detachment 94 | #- nslocalizedstring_key 95 | #- nslocalizedstring_require_bundle 96 | - number_separator 97 | #- object_literal 98 | - one_declaration_per_file # has been added to the source, but has not been released yet 99 | - operator_usage_whitespace 100 | - optional_enum_case_matching 101 | - overridden_super_call 102 | - override_in_extension 103 | - pattern_matching_keywords 104 | #- prefer_nimble 105 | - prefer_self_in_static_references 106 | - prefer_self_type_over_type_of_self 107 | - prefer_zero_over_explicit_init 108 | - prefixed_toplevel_constant 109 | - private_swiftui_state 110 | #- prohibited_interface_builder 111 | #- prohibited_super_call 112 | #- quick_discouraged_call 113 | #- quick_discouraged_focused_test 114 | #- quick_discouraged_pending_test 115 | #- raw_value_for_camel_cased_codable_enum 116 | - reduce_into 117 | - redundant_nil_coalescing 118 | - redundant_self_in_closure 119 | #- redundant_type_annotation 120 | #- required_deinit 121 | #- required_enum_case 122 | - return_value_from_void_function 123 | - self_binding 124 | - shorthand_argument # has been added to the source, but has not been released yet 125 | - shorthand_optional_binding 126 | - single_test_class 127 | #- sorted_enum_cases 128 | - sorted_first_last 129 | #- sorted_imports # see #1295 on github, conflicts with testable, also managed with swiftformat 130 | - static_operator 131 | - strict_fileprivate 132 | #- strong_iboutlet 133 | - superfluous_else 134 | - switch_case_on_newline 135 | - test_case_accessibility 136 | - toggle_bool 137 | - trailing_closure 138 | - type_contents_order 139 | - unavailable_function 140 | - unhandled_throwing_task 141 | - unneeded_parentheses_in_closure_argument 142 | - unowned_variable_capture 143 | - untyped_error_in_catch 144 | #- unused_capture_list # deprecated 145 | #- vertical_parameter_alignment_on_call 146 | #- vertical_whitespace_between_cases 147 | - vertical_whitespace_closing_braces 148 | - vertical_whitespace_opening_braces 149 | - weak_delegate 150 | - xct_specific_matcher 151 | - yoda_condition 152 | 153 | attributes: 154 | always_on_same_line: 155 | - "@IBSegueAction" 156 | - "@IBAction" 157 | - "@NSManaged" 158 | - "@objc" 159 | always_on_line_above: 160 | - "@discardableResult" 161 | 162 | force_cast: error 163 | 164 | force_try: error 165 | 166 | function_body_length: 167 | warning: 150 168 | 169 | legacy_hashing: error 170 | 171 | identifier_name: 172 | min_length: 2 173 | max_length: 174 | warning: 60 175 | error: 80 176 | excluded: 177 | - id 178 | 179 | multiline_arguments: 180 | first_argument_location: any_line 181 | only_enforce_after_first_closure_on_first_line: true 182 | 183 | number_separator: 184 | minimum_length: 5 185 | 186 | overridden_super_call: 187 | excluded: 188 | - setUp() 189 | - setUpWithError() 190 | - tearDown() 191 | - tearDownWithError() 192 | 193 | private_over_fileprivate: 194 | validate_extensions: true 195 | 196 | trailing_whitespace: 197 | ignores_empty_lines: true 198 | ignores_comments: true 199 | 200 | type_name: 201 | min_length: 3 202 | max_length: 203 | warning: 70 204 | error: 80 205 | allowed_symbols: 206 | - "_" 207 | 208 | trailing_closure: 209 | only_single_muted_parameter: true 210 | 211 | cyclomatic_complexity: 212 | ignores_case_statements: true 213 | 214 | function_parameter_count: 215 | warning: 6 216 | error: 8 217 | 218 | type_body_length: 219 | warning: 300 220 | error: 400 -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-version-compare.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Added 2 | 3 | - Dependabot for checking `gihub-actions` updates 4 | - Support for VisionOS 5 | - `.spi.yml` for automated DocC generation on the Swift Package Index 6 | 7 | ### Changed 8 | 9 | - Documentation syntax to DocC 10 | - `SwiftLint` to `0.55.1` 11 | - `SwiftFormat` to `0.54.0` 12 | 13 | ### Removed 14 | 15 | - `jazzy` documentation generation 16 | 17 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 18 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mflknr -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marius Felkner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 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: "swift-version-compare", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .watchOS(.v7), 12 | .tvOS(.v13), 13 | .visionOS(.v1) 14 | ], 15 | products: [ 16 | .library( 17 | name: "VersionCompare", 18 | targets: [ 19 | "VersionCompare" 20 | ] 21 | ) 22 | ], 23 | targets: [ 24 | .target( 25 | name: "VersionCompare", 26 | path: "Sources", 27 | resources: [ 28 | .copy("Resources/PrivacyInfo.xcprivacy") 29 | ] 30 | ), 31 | .testTarget( 32 | name: "VersionCompareTests", 33 | dependencies: [ 34 | "VersionCompare" 35 | ] 36 | ) 37 | ], 38 | swiftLanguageVersions: [.v5] 39 | ) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-version-compare 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmflknr%2Fswift-version-compare%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/mflknr/swift-version-compare) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmflknr%2Fswift-version-compare%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/mflknr/swift-version-compare) 5 | [![](https://github.com/mflknr/swift-version-compare/workflows/checks/badge.svg)](https://github.com/mflknr/swift-version-compare/actions/workflows/checks.yml) 6 | [![licence](https://img.shields.io/github/license/mflknr/swift-version-compare)](https://github.com/mflknr/swift-version-compare/blob/main/LICENSE) 7 | 8 | A package introducing a `Version` object implementing the `SemanticVersionComparable` protocol for comparing versions conforming to [SemVer](https://semver.org). 9 | 10 | # Installation 11 | 12 | #### Swift Package Manager: 13 | 14 | ```swift 15 | .package(url: "https://github.com/mflknr/swift-version-compare.git", from: "2.0.0")) 16 | ``` 17 | 18 | # Usage 19 | 20 | For detailed implementation information see [documentation](https://mflknr.github.io/swift-version-compare/). 21 | 22 | ```swift 23 | // use the version core identifier for initialization 24 | let versionOne = Version(1, 0, 0) 25 | let versionTwo = Version( 26 | major: 1, 27 | minor: 0, 28 | patch: 0, 29 | prerelease: [.alpha], 30 | build: ["1"] 31 | ) // -> prints: "1.0.0-alpha+1" 32 | 33 | // use strings 34 | // use `ExpressibleByStringLiteral` with caution, because it's fatal if string is not `SemVer` version 35 | let versionThreeA: Version? = "1.0.0" 36 | let versionThreeB: Version? = Version("1.0.0") 37 | 38 | // easy initial `0.0.0` version 39 | let initialVersion: Version = .initial 40 | 41 | // from bundle and processInfo 42 | let bundleVersion = Bundle.main.shortVersion 43 | let osVersion = ProcessInfo.processInfo.comparableOperatingSystemVersion 44 | 45 | // compare versions with usally known operators (==, ===, <, <=, >=, >) 46 | if Version("1.0.0") > Version("0.4.0") { 47 | // ... 48 | } 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /Sources/Helper/Character+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Character+Extensions.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 30.03.21. 6 | // 7 | 8 | extension Character { 9 | var isZero: Bool { 10 | if self == "0" { 11 | return true 12 | } 13 | return false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Helper/String+Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Regex.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 05.01.21. 6 | // 7 | 8 | extension String { 9 | var isAlphaNumericString: Bool { 10 | matches("^[a-zA-Z0-9-]+$") 11 | } 12 | 13 | var isNumericString: Bool { 14 | matches("[0-9]+$") 15 | } 16 | 17 | func matches(_ regex: String) -> Bool { 18 | range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil 19 | } 20 | 21 | func matchesSemVerFormat() -> Bool { 22 | matches("^([0-9a-zA-Z]+)\\.([0-9a-zA-Z]+)\\.([0-9a-zA-Z]+)$") || 23 | matches("^([0-9a-zA-Z]+)\\.([0-9a-zA-Z]+)$") || 24 | matches("^([0-9a-zA-Z]+)$") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Helper/VersionCompareResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionCompareResult.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 06.01.21. 6 | // 7 | 8 | /// The severity of an update between versions. 9 | /// 10 | /// - Note: A difference between ``BuildMetaData`` of versions are as `SemVer` states explicitly ignored. 11 | public enum VersionCompareResult { 12 | /// A `MAJOR`update 13 | case major 14 | /// A `MINOR`update 15 | case minor 16 | /// A `PATCH`update 17 | case patch 18 | /// A pre-release update 19 | case prerelease 20 | /// A build update 21 | case build 22 | /// The version is not an update (less or equal) 23 | case noUpdate 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Helper/VersionValidationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionValidationError.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 29.12.20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum VersionValidationError: Swift.Error { 11 | case invalidVersionIdentifier 12 | } 13 | 14 | // MARK: LocalizedError 15 | 16 | extension VersionValidationError: LocalizedError { 17 | var errorDescription: String? { 18 | switch self { 19 | case .invalidVersionIdentifier: 20 | return NSLocalizedString( 21 | "The parsed string contained an invalid SemVer version identifier.", 22 | comment: "" 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyCollectedDataTypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/BuildMetaData/BuildMetaData+ExpressibleByLiteral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildMetaData+ExpressibleByLiteral.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 12.03.21. 6 | // 7 | 8 | // MARK: - BuildMetaData + LosslessStringConvertible 9 | 10 | extension BuildMetaData: LosslessStringConvertible { 11 | public var description: String { value } 12 | 13 | public init?(_ string: String) { 14 | self.init(private: string) 15 | } 16 | } 17 | 18 | // MARK: - BuildMetaData + ExpressibleByStringLiteral 19 | 20 | extension BuildMetaData: ExpressibleByStringLiteral { 21 | public init(stringLiteral value: StringLiteralType) { 22 | self.init(private: value) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/BuildMetaData/BuildMetaData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildMetaData.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 12.03.21. 6 | // 7 | 8 | /// Enumerated ``BuildMetaData`` for simple and `SemVer` conform access. 9 | /// 10 | /// - Note: Identifier can be described using alphanumeric letters or digits. 11 | /// 12 | /// - Attention: Strings not conforming to `SemVer` will be handled as `nil`. 13 | public enum BuildMetaData: Comparable, Sendable { 14 | /// Alphanumeric identifier are lower- and uppercased letters and numbers from 0-9. 15 | case alphaNumeric(_ identifier: String) 16 | 17 | /// Digit identifier are positive numbers and zeros, thus allowing leading zeros. 18 | case digits(_ digits: String) 19 | 20 | /// Unknown identifier are used when string literals do not conform to `SemVer` and are removed. 21 | case unknown 22 | 23 | init(private string: String) { 24 | if Int(string) != nil { 25 | self = .digits(string) 26 | } else if string.isAlphaNumericString { 27 | self = .alphaNumeric(string) 28 | } else { 29 | self = .unknown 30 | } 31 | } 32 | } 33 | 34 | public extension BuildMetaData { 35 | /// Raw string representation of a build-meta-data. 36 | var value: String { 37 | switch self { 38 | case let .alphaNumeric(identifier): 39 | return identifier 40 | case let .digits(identifier): 41 | return identifier 42 | case .unknown: 43 | return "" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/PrereleaseIdentifier/PrereleaseIdentifier+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrereleaseIdentifier+Equatable.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 12.03.21. 6 | // 7 | 8 | public extension PrereleaseIdentifier { 9 | /// Compares pre-release identifiers for equality. 10 | /// 11 | /// - Returns: `true` if pre-release identifiers are equal. 12 | static func == (lhs: Self, rhs: Self) -> Bool { 13 | lhs.value == rhs.value 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/PrereleaseIdentifier/PrereleaseIdentifier+ExpressibleByLiteral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrereleaseIdentifier+ExpressibleByLiteral.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 12.03.21. 6 | // 7 | 8 | // MARK: - PrereleaseIdentifier + LosslessStringConvertible 9 | 10 | extension PrereleaseIdentifier: LosslessStringConvertible { 11 | public var description: String { value } 12 | 13 | public init?(_ string: String) { 14 | self.init(private: string) 15 | } 16 | } 17 | 18 | // MARK: - PrereleaseIdentifier + ExpressibleByStringLiteral 19 | 20 | extension PrereleaseIdentifier: ExpressibleByStringLiteral { 21 | public init(stringLiteral value: StringLiteralType) { 22 | self.init(private: value) 23 | } 24 | } 25 | 26 | // MARK: - PrereleaseIdentifier + ExpressibleByIntegerLiteral 27 | 28 | extension PrereleaseIdentifier: ExpressibleByIntegerLiteral { 29 | public init(integerLiteral value: IntegerLiteralType) { 30 | let absoluteInteger: Int = abs(value) 31 | let unsignedInteger = UInt(absoluteInteger) 32 | self = .numeric(unsignedInteger) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/PrereleaseIdentifier/PrereleaseIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrereleaseIdentifier.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 12.03.21. 6 | // 7 | 8 | /// Enumerated pre-release identifier for `SemVer`. 9 | /// 10 | /// - Note: Identifier can be described using alphanumeric or numeric letters. 11 | /// 12 | /// - Attention: If an identifier does not show conformance for beeing numeric or alphanumeric it is initialized 13 | /// as `nil`. 14 | public enum PrereleaseIdentifier: Comparable, Hashable, Sendable { 15 | /// Identifier displaying `alpha`. 16 | case alpha 17 | 18 | /// Identifier displaying `beta`. 19 | case beta 20 | 21 | /// Identifier displaying `prerelease`. 22 | case prerelease 23 | 24 | /// Identifier displaying `rc`. 25 | case releaseCandidate 26 | 27 | /// Alphanumeric identifier are lower- and uppercased letters and numbers from 0-9. 28 | case alphaNumeric(_ identifier: String) 29 | 30 | /// Numeric identifier are positive numbers and zeros, yet they do not allow for leading zeros. 31 | case numeric(_ identifier: UInt) 32 | 33 | /// Unknown identifier are used when string literals do not conform to `SemVer` and are removed. 34 | case unknown 35 | 36 | init(private string: String) { 37 | if string.isNumericString, 38 | let numeric = UInt(string) 39 | { 40 | self = .numeric(numeric) 41 | } else if string.isAlphaNumericString { 42 | self = .alphaNumeric(string) 43 | } else { 44 | self = .unknown 45 | } 46 | } 47 | } 48 | 49 | public extension PrereleaseIdentifier { 50 | /// Raw string representation of a pre-release identifier. 51 | var value: String { 52 | switch self { 53 | case .alpha: 54 | return "alpha" 55 | case .beta: 56 | return "beta" 57 | case .prerelease: 58 | return "prerelease" 59 | case .releaseCandidate: 60 | return "rc" 61 | case let .alphaNumeric(identifier): 62 | return identifier 63 | case let .numeric(identifier): 64 | return String(identifier) 65 | case .unknown: 66 | return "" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/SemanticVersionComparable+Comparable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemanticVersionComparable+Comparable.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 05.01.21. 6 | // 7 | 8 | public extension SemanticVersionComparable { 9 | /// Compare versions using the `SemVer` ranking system. 10 | /// 11 | /// - Note: ``BuildMetaData`` have no influence on a version's rank. 12 | static func < (lhs: Self, rhs: Self) -> Bool { 13 | // if versions are identical on major, minor and patch level, compare them lexicographiocally 14 | guard lhs.hasEqualVersionCore(as: rhs) else { 15 | // cast UInt to Int for each identifier to compare ordering lexicographically. missing 16 | // identifier for minor or patch versions (e. g. "1" or "2.0") are handled as zeros. 17 | let lhsAsIntSequence: [Int] = [Int(lhs.major), Int(lhs.minor ?? 0), Int(lhs.patch ?? 0)] 18 | let rhsAsIntSequence: [Int] = [Int(rhs.major), Int(rhs.minor ?? 0), Int(rhs.patch ?? 0)] 19 | return lhsAsIntSequence.lexicographicallyPrecedes(rhsAsIntSequence) 20 | } 21 | 22 | // non-pre-release lhs version is always >= than rhs version 23 | guard 24 | let lhspr = lhs.prerelease, 25 | !lhspr.isEmpty 26 | else { 27 | return false 28 | } 29 | 30 | // same goes for rhs vise versa 31 | guard 32 | let rhspr = rhs.prerelease, 33 | !rhspr.isEmpty 34 | else { 35 | return true 36 | } 37 | 38 | // compare content of pre-release identifier 39 | for (untypedLhs, untypedRhs) in zip(lhspr, rhspr) { 40 | // if both pre-release identifier are equal, skip the now obsolete comparison 41 | if untypedLhs == untypedRhs { 42 | continue 43 | } 44 | 45 | // cast identifiers to int or string as Any 46 | let typedLhs: Any = Int(untypedLhs.value) ?? untypedLhs.value 47 | let typedRhs: Any = Int(untypedRhs.value) ?? untypedRhs.value 48 | 49 | switch (typedLhs, typedRhs) { 50 | case let (intLhs as Int, intRhs as Int): 51 | // numerics are compared numerically 52 | return intLhs < intRhs 53 | case let (stringLhs as String, stringRhs as String): 54 | // strings alphanumerically using ASCII 55 | return stringLhs < stringRhs 56 | case (is Int, is String): 57 | // numeric pre-releases are lesser than string pre-releases 58 | return true 59 | case (is String, is Int): 60 | return false 61 | default: 62 | // since we are relatively type safe at this point, it is save to assume we will never 63 | // enter here. so do nothing 64 | () 65 | } 66 | } 67 | 68 | // lastly, if number of identifiers of lhs version is lower than rhs version, it ranks lower 69 | return lhspr.count < rhspr.count 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/SemanticVersionComparable+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemanticVersionComparable+Equatable.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 05.01.21. 6 | // 7 | 8 | public extension SemanticVersionComparable { 9 | /// Compares types conforming to ``SemanticVersionComparable`` for equality. 10 | /// 11 | /// - Returns: `true` if version objects are equal. 12 | static func == (lhs: Self, rhs: Self) -> Bool { 13 | lhs.major == rhs.major 14 | && lhs.minor ?? 0 == rhs.minor ?? 0 15 | && lhs.patch ?? 0 == rhs.patch ?? 0 16 | && lhs.prerelease == rhs.prerelease 17 | } 18 | 19 | /// Strictly compares types conforming to``SemanticVersionComparable`` for equality. 20 | /// 21 | /// - Returns: `true` if version objects are strictly equal. 22 | static func === (lhs: Self, rhs: Self) -> Bool { 23 | lhs.major == rhs.major 24 | && lhs.minor ?? 0 == rhs.minor ?? 0 25 | && lhs.patch ?? 0 == rhs.patch ?? 0 26 | && lhs.prerelease == rhs.prerelease 27 | && lhs.build == rhs.build 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/SemanticVersionComparable+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemanticVersionComparable+Hashable.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 13.03.21. 6 | // 7 | 8 | public extension SemanticVersionComparable { 9 | /// Conformance to `Hashable` protocol. 10 | /// 11 | /// - Note: Since ``BuildMetaData`` are not considered in ranking semantic version, it won't be considered 12 | /// here either. 13 | func hash(into hasher: inout Hasher) { 14 | hasher.combine(major) 15 | hasher.combine(minor) 16 | hasher.combine(patch) 17 | hasher.combine(prerelease) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/SemanticVersionComparable/SemanticVersionComparable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemanticVersionComparable.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 29.12.20. 6 | // 7 | 8 | /// A type that can be expressed and utilized as a semantic version conforming to `SemVer`. 9 | /// 10 | /// Additionally to the ranking and comparison rules if their version core identifiers are `nil` they 11 | /// will be treated as `0`. 12 | /// 13 | /// let versionOne = Version(1, 0, 0) 14 | /// let versionTwo = Version(1) 15 | /// 16 | /// versionOne == versionTwo // <- this statement is `true` 17 | /// 18 | /// You can choose between a loosly or strictly comparison considering if you want to include the ``BuildMetaData`` of 19 | /// versions when comparing: 20 | /// 21 | /// let versionOne = Version(1, 0, 0, [.alpha]) 22 | /// let versionTwo = Version(1, 0, 0, [.alpha], ["exp"]) 23 | /// 24 | /// versionOne == versionTwo // `true` 25 | /// versionOne === versionTwo // `false` 26 | /// 27 | /// - Remark: See [semver.org](https://semver.org) for detailed information. 28 | public protocol SemanticVersionComparable: Comparable, Hashable { 29 | /// The `MAJOR` identifier of a version. 30 | var major: UInt { get } 31 | /// The `MINOR` identifier of a version 32 | var minor: UInt? { get } 33 | /// The `PATCH` identifer of a verion. 34 | var patch: UInt? { get } 35 | 36 | /// Pre-release identifier of a version. 37 | var prerelease: [PrereleaseIdentifier]? { get } 38 | /// Build-meta-data of a version. 39 | var build: [BuildMetaData]? { get } 40 | } 41 | 42 | // MARK: - 43 | 44 | public extension SemanticVersionComparable { 45 | /// A boolean value indicating the compatibility of two versions. As `SemVer` states two versions are 46 | /// compatible if they have the same major version. 47 | /// 48 | /// - Parameter version: A version object that conforms to the ``SemanticVersionComparable`` protocol. 49 | /// 50 | /// - Returns: `true` if both versions have equal major versions. 51 | func isCompatible(with version: Self) -> Bool { 52 | major == version.major 53 | } 54 | 55 | /// Compare a object (lhs) conforming to ``SemanticVersionComparable`` with a greater version object (rhs). 56 | /// Lhs must be a lower version to return a valid result. Otherwise `VersionCompareResult.noUpdate` will be 57 | /// returned regardless of the difference between the two version objects. 58 | /// 59 | /// - Parameter version: A version object that conforms to the ``SemanticVersionComparable`` protocol that will be 60 | /// compared. 61 | /// 62 | /// - Returns: A `VersionCompareResult` as the severity of the update. 63 | func compare(with version: Self) -> VersionCompareResult { 64 | let lhs: Self = self 65 | let rhs: Self = version 66 | 67 | guard !lhs.hasEqualVersionCore(as: rhs) else { 68 | if lhs < rhs { 69 | return .prerelease 70 | } 71 | if lhs.build != rhs.build, 72 | lhs.prereleaseIdentifierString == rhs.prereleaseIdentifierString 73 | { 74 | return .build 75 | } 76 | 77 | return .noUpdate 78 | } 79 | 80 | if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch ?? 0 < rhs.patch ?? 0 { 81 | return .patch 82 | } 83 | 84 | if lhs.major == rhs.major, lhs.minor ?? 0 < rhs.minor ?? 0 { 85 | return .minor 86 | } 87 | 88 | if lhs.major < rhs.major { 89 | return .major 90 | } 91 | 92 | return .noUpdate 93 | } 94 | 95 | /// Check if a version has an equal version core as another version. 96 | /// 97 | /// - Parameter version: A version object that conforms to the ``SemanticVersionComparable`` protocol. 98 | /// 99 | /// - Returns: `true` if the respective version cores are equal. 100 | /// 101 | /// - Note: A version core is defined as the `MAJOR.MINOR.PATCH` part of a semantic version. 102 | func hasEqualVersionCore(as version: Self) -> Bool { 103 | let lhsAsIntSequence: [Int] = [Int(major), Int(minor ?? 0), Int(patch ?? 0)] 104 | let rhsAsIntSequence: [Int] = [Int(version.major), Int(version.minor ?? 0), Int(version.patch ?? 0)] 105 | return lhsAsIntSequence.elementsEqual(rhsAsIntSequence) 106 | } 107 | } 108 | 109 | // MARK: - Accessors 110 | 111 | public extension SemanticVersionComparable { 112 | /// The absolute string of the version containing the core version, pre-release identifier and build-meta-data 113 | /// formatted as `MAJOR.MINOR.PATCH-PRERELEASE+BUILD`. 114 | var absoluteString: String { 115 | var versionString: String = coreString 116 | if let pr = prereleaseIdentifierString { 117 | versionString = [versionString, pr].joined(separator: "-") 118 | } 119 | 120 | if let build = buildMetaDataString { 121 | versionString = [versionString, build].joined(separator: "+") 122 | } 123 | 124 | return versionString 125 | } 126 | 127 | /// The string of the version representing `MAJOR.MINOR.PATCH` only. 128 | var coreString: String { 129 | [major, minor, patch] 130 | .compactMap { $0 } 131 | .map(String.init) 132 | .joined(separator: ".") 133 | } 134 | 135 | /// The string of the version containing the pre-release identifier and build-meta-data only. 136 | var extensionString: String? { 137 | var extensionsString: String? = prereleaseIdentifierString 138 | if let build = buildMetaDataString { 139 | if let ext = extensionsString { 140 | extensionsString = [ext, build].joined(separator: "+") 141 | } else { 142 | extensionsString = build 143 | } 144 | } 145 | 146 | return extensionsString 147 | } 148 | 149 | /// The pre-release identifier as a string if available. 150 | var prereleaseIdentifierString: String? { 151 | prerelease? 152 | .compactMap { $0.value } 153 | .joined(separator: ".") 154 | } 155 | 156 | /// The build-meta-data as a string if available. 157 | var buildMetaDataString: String? { 158 | build? 159 | .compactMap { $0.value } 160 | .joined(separator: ".") 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/Version+Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version+Bundle.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 05.01.21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Bundle { 11 | /// The ``Version`` of the current bundle. 12 | /// 13 | /// - Note: Uses the key `CFBundleShortVersionString` for retrieving version values. 14 | var shortVersion: Version? { 15 | guard let versionString: String = infoDictionary?["CFBundleShortVersionString"] as? String else { 16 | return nil 17 | } 18 | let version: Version? = Version(versionString) 19 | 20 | return version 21 | } 22 | 23 | /// The full ``Version`` of the current bundle. 24 | /// 25 | /// - Note: Uses the key `CFBundleShortVersionString` and `CFBundleVersion` for retrieving version values. 26 | var version: Version? { 27 | guard 28 | let versionString: String = infoDictionary?["CFBundleShortVersionString"] as? String, 29 | let buildString: String = infoDictionary?["CFBundleVersion"] as? String 30 | else { 31 | return nil 32 | } 33 | let fullVersionString = "\(versionString)+\(buildString)" 34 | let version: Version? = Version(fullVersionString) 35 | 36 | return version 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Version+OS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version+OS.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 06.01.21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension ProcessInfo { 11 | /// The ``Version`` of the operating system on which the current process is executing. 12 | @available(macOS, introduced: 10.10) 13 | var comparableOperatingSystemVersion: Version { 14 | let osVersion: OperatingSystemVersion = operatingSystemVersion 15 | let version = Version( 16 | major: UInt(osVersion.majorVersion), 17 | minor: UInt(osVersion.minorVersion), 18 | patch: UInt(osVersion.patchVersion) 19 | ) 20 | 21 | return version 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Version+StringInitializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version+StringInitializer.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 05.01.21. 6 | // 7 | 8 | // MARK: - Version + LosslessStringConvertible 9 | 10 | extension Version: LosslessStringConvertible { 11 | public var description: String { absoluteString } 12 | 13 | /// Creates a new ``Version`` from a string. 14 | /// 15 | /// - Parameter string: A string beeing parsed into a version. 16 | /// 17 | /// - Returns: A version object or `nil` if string does not conform to `SemVer`. 18 | public init?(_ string: String) { 19 | self.init(private: string) 20 | } 21 | } 22 | 23 | // MARK: - Version + ExpressibleByStringLiteral 24 | 25 | extension Version: ExpressibleByStringLiteral { 26 | /// Creates a new ``Version`` from a string literal. 27 | /// 28 | /// - Warning: Usage is not recommended unless the given string conforms to `SemVer`. 29 | public init(stringLiteral value: StringLiteralType) { 30 | // swiftlint:disable:next force_unwrapping 31 | self.init(private: value)! 32 | } 33 | } 34 | 35 | // MARK: - Version + ExpressibleByStringInterpolation 36 | 37 | extension Version: ExpressibleByStringInterpolation { 38 | /// Creates a new ``Version`` from a string interpolation. 39 | /// 40 | /// - Warning: Usage is not recommended unless the given string conforms to `SemVer`. 41 | public init(stringInterpolation: DefaultStringInterpolation) { 42 | // swiftlint:disable:next force_unwrapping compiler_protocol_init 43 | self.init(private: String(stringInterpolation: stringInterpolation))! 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version.swift 3 | // SwiftVersionCompare 4 | // 5 | // Created by Marius Felkner on 29.12.20. 6 | // 7 | 8 | /// A version type conforming to ``SemanticVersionComparable`` and therefor `SemVer`. 9 | /// 10 | /// You can create a new version using strings, string literals and string interpolations, formatted 11 | /// like `MAJOR.MINOR.PATCH-PRERELEASE+BUILD`, or memberwise initialization. 12 | /// 13 | /// // from string 14 | /// let version: Version? = "1.0.0" 15 | /// let version: Version? = Version("1.0.0-alpha.1+23") 16 | /// let version: Version? = Version("1.0.0.1.2") // <- will be `nil` since it's not `SemVer` 17 | /// 18 | /// let version: Version = "1.0.0" // <- will crash if string does not conform to `SemVer` 19 | /// 20 | /// // from memberwise properties 21 | /// let version: Version = Version(1, 0, 0) 22 | /// let version: Version = Version(major: 1, minor: 0, patch: 0, prerelease: ["alpha, "1"], build: ["exp"]) 23 | /// 24 | /// Pre-release identifiers or ``BuildMetaData`` can be handled as strings or as enum cases with it associated raw 25 | /// values (see ``PrereleaseIdentifier`` and ``BuildMetaData`` for more). 26 | /// 27 | /// let version: Version = Version(major: 1, minor: 0, patch: 0, prerelease: ["alpha"], build: ["500"]) 28 | /// version.absoluteString // -> "1.0.0-alpha+500" 29 | /// 30 | /// let version: Version = Version(2, 32, 16, ["family", .alpha], ["1"]) 31 | /// version.absoluteString // -> "2.32.16-family.alpha+1" 32 | /// version.coreString // -> "2.32.16" 33 | /// version.extensionString // -> "family.alpha+1" 34 | /// version.prereleaseIdentifer // -> "family.alpha" 35 | /// version.buildMetaDataString // -> "1" 36 | /// 37 | /// - Remark: See [semver.org](https://semver.org) for detailed information. 38 | public struct Version: Sendable, SemanticVersionComparable { 39 | public var major: UInt 40 | public var minor: UInt? 41 | public var patch: UInt? 42 | 43 | public var prerelease: [PrereleaseIdentifier]? 44 | public var build: [BuildMetaData]? 45 | 46 | // MARK: - Init 47 | 48 | /// Creates a new ``Version``. 49 | /// 50 | /// - Parameters: 51 | /// - major: The `MAJOR` identifier of a version. 52 | /// - minor: The `MINOR` identifier of a version. 53 | /// - patch: The `PATCH` identifier of a version. 54 | /// - prerelease: The pre-release identifier of a version. 55 | /// - build: The build-meta-data of a version. 56 | /// 57 | /// - Returns: A new version. 58 | /// 59 | /// - Note: Unsigned integers are used to provide an straightforward way to make sure that the identifiers 60 | /// are not negative numbers. 61 | @inlinable 62 | public init( 63 | _ major: UInt, 64 | _ minor: UInt? = nil, 65 | _ patch: UInt? = nil, 66 | _ prerelease: [PrereleaseIdentifier]? = nil, 67 | _ build: [BuildMetaData]? = nil 68 | ) { 69 | self.major = major 70 | self.minor = minor 71 | self.patch = patch 72 | 73 | self.prerelease = prerelease 74 | self.build = build 75 | } 76 | 77 | /// Creates a new ``Version``. 78 | /// 79 | /// - Parameters: 80 | /// - major: The `MAJOR` identifier of a version. 81 | /// - minor: The `MINOR` identifier of a version. 82 | /// - patch: The `PATCH` identifier of a version. 83 | /// - prerelease: The ``PrereleaseIdentifier`` identifier of a version. 84 | /// - build: The ``BuildMetaData`` of a version. 85 | /// 86 | /// - Note: Unsigned integers are used to provide an straightforward way to make sure that the identifiers 87 | /// are not negative numbers. 88 | @inlinable 89 | public init( 90 | major: UInt, 91 | minor: UInt? = nil, 92 | patch: UInt? = nil, 93 | prerelease: [PrereleaseIdentifier]? = nil, 94 | build: [BuildMetaData]? = nil 95 | ) { 96 | self.init(major, minor, patch, prerelease, build) 97 | } 98 | 99 | /// Creates a new ``Version`` using a string. 100 | /// 101 | /// - Parameter string: The string representing a version. 102 | public init?(private string: String) { 103 | // split string into version with pre-release identifier and build-meta-data substrings 104 | let versionSplitBuild: [String.SubSequence] = string.split(separator: "+", omittingEmptySubsequences: false) 105 | 106 | // check if string does not contain only build-meta-data e.g. "+123" or falsely "+123+something" 107 | let maxNumberOfSplits = 2 108 | guard 109 | !versionSplitBuild.isEmpty, 110 | versionSplitBuild.count <= maxNumberOfSplits, 111 | let versionPrereleaseString = versionSplitBuild.first 112 | else { 113 | return nil 114 | } 115 | 116 | // split previously splitted substring into version and pre-release identifier substrings 117 | var versionSplitPrerelease: [Substring.SubSequence] = versionPrereleaseString 118 | .split(separator: "-", omittingEmptySubsequences: false) 119 | 120 | // check for non-empty or invalid version string e.g. "-alpha" 121 | guard 122 | !versionSplitPrerelease.isEmpty, 123 | let versionStringElement = versionSplitPrerelease.first, 124 | !versionStringElement.isEmpty 125 | else { 126 | return nil 127 | } 128 | 129 | // check that the version string has the correct SemVer format which are 0 and positive numbers in the form 130 | // of `x`, `x.x`or `x.x.x`. 131 | let versionString = String(versionStringElement) 132 | guard versionString.matchesSemVerFormat() else { 133 | return nil 134 | } 135 | 136 | // extract version elements from validated version string as unsigned integers, throws and returns nil 137 | // if a substring cannot be casted as UInt, since only positive numbers are allowed 138 | let versionIdentifiers: [UInt]? = try? versionString 139 | .split(separator: ".") 140 | .map(String.init) 141 | .map { 142 | // we already checked the format so we can now try to extract an UInt from the string 143 | guard 144 | let element = UInt($0), 145 | let firstCharacter = $0.first, 146 | !(firstCharacter.isZero && $0.count > 1) 147 | else { 148 | throw VersionValidationError.invalidVersionIdentifier 149 | } 150 | 151 | return element 152 | } 153 | 154 | guard let safeIdentifiers = versionIdentifiers else { 155 | return nil 156 | } 157 | 158 | // map valid identifiers to corresponding version identifier 159 | major = safeIdentifiers[0] 160 | minor = safeIdentifiers.indices.contains(1) ? safeIdentifiers[1] : nil 161 | // swiftlint:disable:next no_magic_numbers 162 | patch = safeIdentifiers.indices.contains(2) ? safeIdentifiers[2] : nil 163 | 164 | // extract pre-release identifier if available 165 | if versionSplitPrerelease.indices.contains(1) { 166 | versionSplitPrerelease.removeFirst(1) 167 | let prereleaseSubstring: String = versionSplitPrerelease.joined(separator: "-") 168 | prerelease = String(prereleaseSubstring) 169 | .split(separator: ".") 170 | .map(String.init) 171 | .compactMap { identifierString in 172 | if let asInt = Int(identifierString) { 173 | return PrereleaseIdentifier(integerLiteral: asInt) 174 | } 175 | 176 | return PrereleaseIdentifier(identifierString) 177 | } 178 | // if a pre-release identifier element is initialized as .unkown, we can savely assume that the given 179 | // string is not a valid `SemVer` version string. 180 | if 181 | let prerelease, 182 | prerelease.contains(where: { $0 == .unknown }) 183 | { 184 | return nil 185 | } 186 | } else { 187 | // not pre-release identifier has been found 188 | prerelease = nil 189 | } 190 | 191 | // extract build-meta-data identifier if available 192 | if 193 | versionSplitBuild.indices.contains(1), 194 | let buildSubstring = versionSplitBuild.last 195 | { 196 | build = String(buildSubstring) 197 | .split(separator: ".") 198 | .map(String.init) 199 | .compactMap { BuildMetaData($0) } 200 | // finding an .unkown element means that the given string is not conform to `SemVer` since it is no 201 | // alphaNumeric or a digit 202 | if 203 | let build, 204 | build.contains(where: { $0 == .unknown }) 205 | { 206 | return nil 207 | } 208 | } else { 209 | // no build-meta-data has been found 210 | build = nil 211 | } 212 | } 213 | } 214 | 215 | // MARK: - Static Accessors 216 | 217 | public extension Version { 218 | /// An initial ``Version`` representing the string `0.0.0`. 219 | static var initial: Version = .init(major: 0, minor: 0, patch: 0) 220 | } 221 | 222 | // MARK: CustomDebugStringConvertible 223 | 224 | extension Version: CustomDebugStringConvertible { 225 | public var debugDescription: String { absoluteString } 226 | } 227 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | XCTMain([XCTestCaseEntry]()) 4 | -------------------------------------------------------------------------------- /Tests/VersionCompareTests/SemanticVersionComparableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SemanticVersionComparableTests.swift 3 | // VersionCompareTests 4 | // 5 | // Created by Marius Felkner on 01.01.21. 6 | // 7 | 8 | import XCTest 9 | @testable import VersionCompare 10 | 11 | final class SemanticVersionComparableTests: XCTestCase { 12 | func testEqualOperator() throws { 13 | let testData: KeyValuePairs = [ 14 | Version("15.287349.10"): Version("15.287349.10"), 15 | Version("0.1.0"): Version("0.1.0"), 16 | Version("1.0.0"): Version("1"), 17 | Version("15.2"): Version("15.2.0"), 18 | Version("1"): Version("1"), 19 | Version("123.0.0"): Version("123"), 20 | Version("1.2"): Version("1.2.0"), 21 | Version("1.9.0"): Version("1.9"), 22 | Version("1-alpha.1"): Version("1-alpha.1"), 23 | Version("24-beta+1"): Version("24-beta+1"), 24 | Version("1.6.2+exp.1"): Version("1.6.2+exp.1"), 25 | Version("2.0+500"): Version("2.0+500"), 26 | Version("300.0+master"): Version("300.0+develop"), 27 | Version(1, nil, nil, [.alpha]): Version(1, 0, 0, ["alpha"]), 28 | Version(1, nil, nil, [.beta]): Version(1, 0, 0, ["beta"]), 29 | Version(1, nil, nil, [.releaseCandidate]): Version(1, 0, 0, ["rc"]), 30 | Version(1, nil, nil, [.prerelease]): Version(1, 0, 0, ["prerelease"]) 31 | ] 32 | 33 | for (lhs, rhs) in testData { 34 | XCTAssertEqual(lhs, rhs, "Expected \(lhs) to be equal to \(rhs)") 35 | XCTAssertFalse(lhs > rhs, "Expected \(lhs) to be greater than \(rhs)") 36 | XCTAssertFalse(lhs < rhs, "Expected \(lhs) to be lesser than \(rhs)") 37 | } 38 | } 39 | 40 | func testStrictEqualOperator() throws { 41 | let validTestData: KeyValuePairs = [ 42 | Version("15.287349.10"): Version("15.287349.10"), 43 | Version("0.1.0"): Version("0.1.0"), 44 | Version("1.0.0"): Version("1"), 45 | Version("15.2"): Version("15.2.0"), 46 | Version("1"): Version("1"), 47 | Version("123.0.0"): Version("123"), 48 | Version("1.2"): Version("1.2.0"), 49 | Version("1.9.0"): Version("1.9"), 50 | Version("1-alpha.1"): Version("1-alpha.1"), 51 | Version("24-beta+1"): Version("24-beta+1"), 52 | Version("1.6.2+exp.1"): Version("1.6.2+exp.1"), 53 | Version("2.0+500"): Version("2.0+500") 54 | ] 55 | 56 | let invalidTestData: [Version: Version] = [ 57 | Version("300.0+master"): Version("300.0+develop") 58 | ] 59 | 60 | for (lhs, rhs) in validTestData { 61 | XCTAssertTrue(lhs === rhs, "Expected \(lhs) to be equal to \(rhs)") 62 | XCTAssertFalse(lhs > rhs, "Expected \(lhs) to be greater than \(rhs)") 63 | XCTAssertFalse(lhs < rhs, "Expected \(lhs) to be lesser than \(rhs)") 64 | } 65 | 66 | for (lhs, rhs) in invalidTestData { 67 | XCTAssertFalse(lhs === rhs, "Expected \(lhs) to be equal to \(rhs)") 68 | } 69 | } 70 | 71 | func testNonEqualOperators() throws { 72 | let testData: KeyValuePairs = [ 73 | Version("15.287349.9"): Version("15.287349.10"), 74 | Version("0.0.1"): Version("0.1.0"), 75 | Version("0"): Version("1.0.0"), 76 | Version("13.6"): Version("15.2.0"), 77 | Version("2"): Version("25"), 78 | Version("777.8987"): Version("777.8988"), 79 | Version("13.9182.0"): Version("15.2.0"), 80 | Version("13.9182.1-alpha"): Version("13.9182.1"), 81 | Version("13.1.1-alpha"): Version("13.1.1-beta"), 82 | Version("5-h2o4hr"): Version("5"), 83 | Version("5-alpha.1"): Version("5-alpha.2"), 84 | Version("5-alpha.2"): Version("5-alpha.beta"), 85 | Version("5-alpha.23+500"): Version("5-alpha.beta+200") 86 | ] 87 | 88 | for (lhs, rhs) in testData { 89 | // less 90 | XCTAssertTrue(lhs < rhs, "Expected \(lhs.absoluteString) to be less to \(rhs.absoluteString)!") 91 | XCTAssertTrue(lhs <= rhs, "Expected \(lhs.absoluteString) to be less or equal to \(rhs.absoluteString)!") 92 | XCTAssertTrue(lhs <= lhs) 93 | XCTAssertTrue(rhs <= rhs) 94 | 95 | XCTAssertFalse(rhs < lhs, "Expected \(rhs.absoluteString) to be less to \(lhs.absoluteString)!") 96 | XCTAssertFalse(lhs < lhs) 97 | XCTAssertFalse(rhs < rhs) 98 | 99 | // greater 100 | XCTAssertTrue(rhs > lhs, "Expected \(lhs.absoluteString) to be greater than \(rhs.absoluteString)!") 101 | XCTAssertTrue( 102 | rhs >= lhs, 103 | "Expected \(lhs.absoluteString) to be greater than or equal to \(rhs.absoluteString)!" 104 | ) 105 | XCTAssertTrue(rhs >= rhs) 106 | XCTAssertTrue(lhs >= lhs) 107 | 108 | XCTAssertFalse(lhs > rhs, "Expected \(lhs.absoluteString) to be greater to \(rhs.absoluteString)!") 109 | XCTAssertFalse(rhs > rhs) 110 | XCTAssertFalse(lhs > lhs) 111 | } 112 | } 113 | 114 | func testCompatibility() { 115 | let versionAA = Version("1.0.0") 116 | let versionAB = Version("1.1.0") 117 | let versionAC = Version("1.1.1") 118 | let versionAD = Version("1.0.1") 119 | let versionAE = Version("1.6238746") 120 | let versionAF = Version("1") 121 | let versionAs: [Version] = [versionAA, versionAB, versionAC, versionAD, versionAE, versionAF] 122 | 123 | let versionBA = Version("2.0.0") 124 | let versionBB = Version("2.1.0") 125 | let versionBC = Version("2.1.1") 126 | let versionBD = Version("2.0.1") 127 | let versionBE = Version("2.3875") 128 | let versionBF = Version("2") 129 | let versionBs: [Version] = [versionBA, versionBB, versionBC, versionBD, versionBE, versionBF] 130 | 131 | // compatible 132 | for first in versionAs { 133 | for second in versionAs { 134 | XCTAssertTrue(first.isCompatible(with: second)) 135 | } 136 | } 137 | 138 | for first in versionBs { 139 | for second in versionBs { 140 | XCTAssertTrue(first.isCompatible(with: second)) 141 | } 142 | } 143 | 144 | // incompatible 145 | for first in versionAs { 146 | for second in versionBs { 147 | XCTAssertFalse(first.isCompatible(with: second)) 148 | } 149 | } 150 | 151 | for first in versionBs { 152 | for second in versionAs { 153 | XCTAssertFalse(first.isCompatible(with: second)) 154 | } 155 | } 156 | } 157 | 158 | func testCompare() { 159 | // swiftlint:disable:next large_tuple 160 | let testData: [(Version, Version, VersionCompareResult)] = [ 161 | (Version("1"), Version("2"), VersionCompareResult.major), 162 | (Version("600.123.4"), Version("601.0.1"), VersionCompareResult.major), 163 | (Version("1.3"), Version("1.5"), VersionCompareResult.minor), 164 | (Version("3.230.13"), Version("3.235.1"), VersionCompareResult.minor), 165 | (Version("565.1.123"), Version("565.1.124"), VersionCompareResult.patch), 166 | (Version("1.2-alpha"), Version("1.2-beta"), VersionCompareResult.prerelease), 167 | (Version("1.34523"), Version("1.34523+100"), VersionCompareResult.build), 168 | (Version("2.235234.1"), Version("1.8967596758.4"), VersionCompareResult.noUpdate), 169 | (Version("2.0.0"), Version("2"), VersionCompareResult.noUpdate), 170 | (Version("2.0.0"), Version("2+1"), VersionCompareResult.build), 171 | (Version("2.0"), Version("2-alpha.beta.1"), VersionCompareResult.noUpdate), 172 | (Version("2.0-alpha.beta.1"), Version("2"), VersionCompareResult.prerelease), 173 | (Version("2.0-alpha.beta.1"), Version("2+exp.1"), VersionCompareResult.prerelease), 174 | (Version("2.0-alpha.beta.1"), Version("2-alpha.beta.1+exp.1"), VersionCompareResult.build), 175 | (Version("2.0-alpha.beta.1"), Version("2.0.0-alpha.beta.1+exp.1"), VersionCompareResult.build), 176 | (Version("2-alpha.beta.1"), Version("2.0-alpha.beta.1+exp.1"), VersionCompareResult.build), 177 | (Version("2-alpha.beta.1"), Version("2-alpha.beta.1+exp.1"), VersionCompareResult.build), 178 | (Version("2-alpha.beta.1+1"), Version("2-alpha.beta.1+exp.1"), VersionCompareResult.build), 179 | (Version("1.0.0-alpha"), Version("1.0.0+1"), VersionCompareResult.prerelease), 180 | (Version("1.0.0+234"), Version("1.0.0-alpha"), VersionCompareResult.noUpdate), 181 | (Version("1.0.0-alpha+1"), Version("1.0.0"), VersionCompareResult.prerelease) 182 | ] 183 | 184 | for data in testData { 185 | let versionOne: Version = data.0 186 | let versionTwo: Version = data.1 187 | let compareResult: VersionCompareResult = versionOne.compare(with: versionTwo) 188 | XCTAssertEqual( 189 | compareResult, 190 | data.2, 191 | "Expected result from comparing to be \(data.2) but is \(compareResult)!" 192 | ) 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Tests/VersionCompareTests/VersionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionTests.swift 3 | // VersionCompareTests 4 | // 5 | // Created by Marius Felkner on 01.01.21. 6 | // 7 | 8 | import XCTest 9 | @testable import VersionCompare 10 | 11 | typealias ValidVersionStringLiteral = String 12 | typealias ExpectedVersionString = String 13 | typealias ExpectedExtensionString = String 14 | 15 | final class VersionTests: XCTestCase { 16 | // swiftlint:disable:next large_tuple 17 | private let validVersionData: [(ValidVersionStringLiteral, ExpectedVersionString, ExpectedExtensionString?)] = [ 18 | ("1.0.0", "1.0.0", nil), 19 | ("1.2.3-alpha.1", "1.2.3", "alpha.1"), 20 | ("1.0", "1.0", nil), 21 | ("1", "1", nil), 22 | ("13434", "13434", nil), 23 | ("0.123123.0", "0.123123.0", nil), 24 | ("0.0.127498127947", "0.0.127498127947", nil), 25 | ("1+1", "1", "1"), 26 | ("1-beta.1+exval30", "1", "beta.1+exval30"), 27 | ("23.400-familyalpha.2.beta+172948712.1", "23.400", "familyalpha.2.beta+172948712.1"), 28 | ("1.5+thomassbuild", "1.5", "thomassbuild"), 29 | ("3.265893.15-alpha.13.beta+exp.sha.315", "3.265893.15", "alpha.13.beta+exp.sha.315"), 30 | ("5", "5", nil), 31 | ("5.3", "5.3", nil), 32 | ("5.3.2", "5.3.2", nil), 33 | ("5.0", "5.0", nil), 34 | ("5.0.0", "5.0.0", nil), 35 | ("5.3.0", "5.3.0", nil), 36 | ("5.3.2", "5.3.2", nil), 37 | ("5+3990", "5", "3990"), 38 | ("5.3+3990", "5.3", "3990"), 39 | ("5.3.2+3990", "5.3.2", "3990"), 40 | ("5.0+3990", "5.0", "3990"), 41 | ("5.0.0+3990", "5.0.0", "3990"), 42 | ("5.3.0+3990", "5.3.0", "3990"), 43 | ("5.3.2+3990", "5.3.2", "3990"), 44 | ("5-rc", "5", "rc"), 45 | ("5.3-rc", "5.3", "rc"), 46 | ("5.3.2-rc", "5.3.2", "rc"), 47 | ("5.0-rc", "5.0", "rc"), 48 | ("5.0.0-rc", "5.0.0", "rc"), 49 | ("5.3.0-rc", "5.3.0", "rc"), 50 | ("5.3.2-rc", "5.3.2", "rc"), 51 | ("5-rc+3990", "5", "rc+3990"), 52 | ("5.3-rc+3990", "5.3", "rc+3990"), 53 | ("5.3.2-rc+3990", "5.3.2", "rc+3990"), 54 | ("5.0-rc+3990", "5.0", "rc+3990"), 55 | ("5.0.0-rc+3990", "5.0.0", "rc+3990"), 56 | ("5.3.0-rc+3990", "5.3.0", "rc+3990"), 57 | ("5.3.2-rc+3990", "5.3.2", "rc+3990"), 58 | ("1-1", "1", "1"), 59 | ("1.2.3-alpha-beta+3", "1.2.3", "alpha-beta+3"), 60 | ("1.0.0-alpha-1skladnk1.1+123", "1.0.0", "alpha-1skladnk1.1+123"), 61 | ("1.0.0-alpha-1skl--------ad---nk1.---+123", "1.0.0", "alpha-1skl--------ad---nk1.---+123"), 62 | ("1.2.3-test+123-123-123-123", "1.2.3", "test+123-123-123-123") 63 | ] 64 | 65 | private let invalidVersionData: [String] = [ 66 | ".0.", 67 | ".0", 68 | ".123", 69 | ".400.", 70 | "1.0.x", 71 | "1.x.0", 72 | "x.0.0", 73 | "", 74 | "ofkn", 75 | "_`'*§!§", 76 | "da.a`sm-k132/89", 77 | "1.1.1.1", 78 | "0.0.0.0.0.0", 79 | ".0.0", 80 | "0.0.", 81 | "alpha", 82 | "-alpha", 83 | "-beta.123", 84 | "-", 85 | "-pre-build", 86 | "sdjflk.ksdjla.123", 87 | "asdasd.1.1", 88 | "1.1.4354vdf", 89 | "18+123+something", 90 | "1.2.3-test+123-123-123-123+", 91 | "0000001.00000001.01111", 92 | "1.1.1-alpha%", 93 | "2-beta+23$" 94 | ] 95 | 96 | func testValidConstruction() { 97 | // swiftlint:disable force_unwrapping 98 | for validVersionData in validVersionData { 99 | let version: Version? = Version(validVersionData.0) 100 | XCTAssertNotNil(version, "Expected object from string `\(validVersionData.0)` not to be nil!") 101 | XCTAssertEqual( 102 | version!.coreString, 103 | validVersionData.1, 104 | "Expected versionCode to be \(validVersionData.1), is: \(version!.coreString)" 105 | ) 106 | XCTAssertEqual(version!.debugDescription, version!.description) 107 | if let expectedExtension = validVersionData.2 { 108 | XCTAssertEqual( 109 | version!.extensionString, 110 | validVersionData.2, 111 | "Expected extension to be \(expectedExtension), is: \(version!.extensionString ?? "nil")" 112 | ) 113 | } else { 114 | XCTAssertNil(version!.extensionString, "Expected extension to be nil!") 115 | } 116 | } 117 | // swiftlint:enable force_unwrapping 118 | 119 | // test string literal 120 | for validVersionData in validVersionData { 121 | // equivalent to `let version: Version = ""` 122 | let version = Version(stringLiteral: validVersionData.0) 123 | XCTAssertNotNil(version, "Expected object from string `\(validVersionData.0)` not to be nil!") 124 | XCTAssertEqual( 125 | version.coreString, 126 | validVersionData.1, 127 | "Expected versionCode to be \(validVersionData.1), is: \(version.coreString)" 128 | ) 129 | XCTAssertEqual(version.debugDescription, version.description) 130 | if let expectedExtension = validVersionData.2 { 131 | XCTAssertEqual( 132 | version.extensionString, 133 | validVersionData.2, 134 | "Expected extension to be \(expectedExtension), is: \(version.extensionString ?? "nil")" 135 | ) 136 | } else { 137 | XCTAssertNil(version.extensionString, "Expected extension to be nil!") 138 | } 139 | } 140 | 141 | // test string interpolation 142 | for validVersionData in validVersionData { 143 | // equivalent to `let version: Version = ""` 144 | let version: Version = "\(validVersionData.0)" 145 | XCTAssertNotNil(version, "Expected object from string `\(validVersionData.0)` not to be nil!") 146 | XCTAssertEqual( 147 | version.coreString, 148 | validVersionData.1, 149 | "Expected versionCode to be \(validVersionData.1), is: \(version.coreString)" 150 | ) 151 | XCTAssertEqual(version.debugDescription, version.description) 152 | if let expectedExtension = validVersionData.2 { 153 | XCTAssertEqual( 154 | version.extensionString, 155 | validVersionData.2, 156 | "Expected extension to be \(expectedExtension), is: \(version.extensionString ?? "nil")" 157 | ) 158 | } else { 159 | XCTAssertNil(version.extensionString, "Expected extension to be nil!") 160 | } 161 | } 162 | } 163 | 164 | func testMemberwiseConstruction() { 165 | let versionA = Version(major: 1, minor: 2, patch: 3, prerelease: [.alpha]) 166 | XCTAssertEqual(versionA.absoluteString, "1.2.3-alpha", "Expected version to be `1.2.3-alpha`, is: \(versionA)!") 167 | 168 | let versionB = Version(major: 125) 169 | XCTAssertEqual(versionB, "125.0.0") 170 | 171 | let versionC = Version( 172 | major: 1, 173 | minor: 2, 174 | patch: 3, 175 | prerelease: [.alpha, "release"], 176 | build: [ 177 | .alphaNumeric("exp"), 178 | .digits("300"), 179 | "test" 180 | ] 181 | ) 182 | XCTAssertEqual( 183 | versionC.absoluteString, 184 | "1.2.3-alpha.release+exp.300.test", 185 | "Expected version to be `1.2.3-alpha.release+exp.300.test`, is: \(versionC)!" 186 | ) 187 | 188 | let versionD = Version( 189 | major: 1, 190 | minor: 2, 191 | patch: 3, 192 | prerelease: [.alphaNumeric("alpha"), .numeric(1), .beta, .releaseCandidate, .prerelease], 193 | build: [.alphaNumeric("exp"), .digits("300"), "test"] 194 | ) 195 | XCTAssertEqual( 196 | versionD.absoluteString, 197 | "1.2.3-alpha.1.beta.rc.prerelease+exp.300.test", 198 | "Expected version to be `1.2.3-alpha.release+exp.300.test`, is: \(versionD)!" 199 | ) 200 | } 201 | 202 | func testInvalidConstruction() { 203 | for invalidVersionData in invalidVersionData { 204 | XCTAssertNil(Version(invalidVersionData), "Expected object from string `\(invalidVersionData)` to be nil!") 205 | } 206 | } 207 | 208 | func testProcessInfoVersion() { 209 | let processInfoOsVersion: OperatingSystemVersion = ProcessInfo.processInfo.operatingSystemVersion 210 | let comparableOsVersion: Version = ProcessInfo.processInfo.comparableOperatingSystemVersion 211 | 212 | XCTAssertEqual( 213 | UInt(processInfoOsVersion.majorVersion), 214 | comparableOsVersion.major, 215 | "Expected \(processInfoOsVersion.majorVersion) to be equal to \(comparableOsVersion.major)!" 216 | ) 217 | XCTAssertEqual( 218 | UInt(processInfoOsVersion.minorVersion), 219 | comparableOsVersion.minor, 220 | // swiftlint:disable:next force_unwrapping 221 | "Expected \(processInfoOsVersion.minorVersion) to be equal to \(comparableOsVersion.minor!)!" 222 | ) 223 | XCTAssertEqual( 224 | UInt(processInfoOsVersion.patchVersion), 225 | comparableOsVersion.patch, 226 | // swiftlint:disable:next force_unwrapping 227 | "Expected \(processInfoOsVersion.patchVersion) to be equal to \(comparableOsVersion.patch!)!" 228 | ) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Tests/VersionCompareTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | // swiftlint:disable:next missing_docs 5 | public func allTests() -> [XCTestCaseEntry] { 6 | [] 7 | } 8 | #endif 9 | --------------------------------------------------------------------------------