├── .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://swiftpackageindex.com/mflknr/swift-version-compare)
4 | [](https://swiftpackageindex.com/mflknr/swift-version-compare)
5 | [](https://github.com/mflknr/swift-version-compare/actions/workflows/checks.yml)
6 | [](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 |
--------------------------------------------------------------------------------