Skip to content

Commit face628

Browse files
committed
Move version comparison out of string and into a separate class
1 parent 1e7b070 commit face628

File tree

5 files changed

+146
-134
lines changed

5 files changed

+146
-134
lines changed

WooCommerce/Classes/Extensions/String+Helpers.swift

Lines changed: 0 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -68,131 +68,4 @@ extension String {
6868
var isNotEmpty: Bool {
6969
return !isEmpty
7070
}
71-
72-
/// Compares two strings as versions.
73-
///
74-
/// Returns `orderedAscending` if the argument represents a newer version
75-
/// Returns `orderedSame` if the argument represents the same version
76-
/// Returns `orderedDescending` if the argument represents an older version
77-
///
78-
func compareAsVersion(to: String) -> ComparisonResult {
79-
80-
/// Replace _ - and + with a .
81-
///
82-
func replaceUnderscoreDashAndPlusWithDot(_ string: String) -> String {
83-
string.replacingOccurrences(of: "([_\\-+]+)", with: ".", options: .regularExpression)
84-
}
85-
86-
/// Insert a . before and after any non number
87-
///
88-
func insertDotsBeforeAndAfterAnyNonNumber(_ string: String) -> String {
89-
string.replacingOccurrences(of: "([^0-9.]+)", with: ".$1.", options: .regularExpression)
90-
}
91-
92-
/// Score and compare two string components
93-
///
94-
func compareStringComponents(_ lhs: String, _ rhs: String) -> ComparisonResult {
95-
/// Score each component
96-
let lhsScore = VersionComponentScore(from: lhs)
97-
let rhsScore = VersionComponentScore(from: rhs)
98-
99-
if lhsScore < rhsScore {
100-
return .orderedAscending
101-
}
102-
103-
if lhsScore > rhsScore {
104-
return .orderedDescending
105-
}
106-
107-
if lhsScore == .number && rhsScore == .number {
108-
let lhsAsNumber = NSNumber(value: Int(lhs) ?? 0)
109-
let rhsAsNumber = NSNumber(value: Int(rhs) ?? 0)
110-
111-
let comparisonResult = lhsAsNumber.compare(rhsAsNumber)
112-
if comparisonResult != .orderedSame {
113-
return comparisonResult
114-
}
115-
}
116-
117-
return .orderedSame
118-
}
119-
120-
/// Process the given string into version components
121-
///
122-
func versionComponents(of string: String) -> [String] {
123-
var stringToComponentize = replaceUnderscoreDashAndPlusWithDot(string)
124-
stringToComponentize = insertDotsBeforeAndAfterAnyNonNumber(stringToComponentize)
125-
return stringToComponentize.components(separatedBy: ".")
126-
}
127-
128-
let leftComponents = versionComponents(of: self)
129-
let rightComponents = versionComponents(of: to)
130-
131-
let maxComponents = max(leftComponents.count, rightComponents.count)
132-
133-
for index in 0..<maxComponents {
134-
/// Treat missing components (e.g. 1.2 being compared to 1.1.3 as "0", i.e. 1.2.0
135-
let leftComponent = index < leftComponents.count ? leftComponents[index] : "0"
136-
let rightComponent = index < rightComponents.count ? rightComponents[index] : "0"
137-
138-
let comparisonResult = compareStringComponents(leftComponent, rightComponent)
139-
if comparisonResult != .orderedSame {
140-
return comparisonResult
141-
}
142-
}
143-
144-
return .orderedSame
145-
}
146-
}
147-
148-
/// Defines the score (rank) of a component string within a version string.
149-
/// e.g. the "3" in 3.0.0beta3 should be treated as `.number`
150-
/// and the "beta" should be scored (ranked) lower as `.beta`.
151-
///
152-
/// The scores of components of version strings are used when comparing complete version strings
153-
/// to decide if one version is older, equal or newer than another.
154-
///
155-
/// Ranked per https://www.php.net/manual/en/function.version-compare.php
156-
///
157-
fileprivate enum VersionComponentScore: Comparable {
158-
case other
159-
case dev
160-
case alpha
161-
case beta
162-
case RC
163-
case number
164-
case patch
165-
}
166-
167-
extension VersionComponentScore {
168-
init(from: String) {
169-
if from == "dev" {
170-
self = .dev
171-
return
172-
}
173-
if from == "alpha" || from == "a" {
174-
self = .alpha
175-
return
176-
}
177-
if from == "beta" || from == "b" {
178-
self = .beta
179-
return
180-
}
181-
if from == "RC" || from == "rc" {
182-
self = .RC
183-
return
184-
}
185-
let componentCharacterSet = CharacterSet(charactersIn: from)
186-
if componentCharacterSet.isSubset(of: .decimalDigits) {
187-
self = .number
188-
return
189-
}
190-
191-
if from == "pl" || from == "p" {
192-
self = .patch
193-
return
194-
}
195-
196-
self = .other
197-
}
19871
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import Foundation
2+
3+
/// Helpers for working with versions (e.g. comparing two version strings)
4+
///
5+
final class VersionHelpers {
6+
private init() {}
7+
8+
/// Compares two strings as versions using the same approach as PHP `version_compare`.
9+
/// https://www.php.net/manual/en/function.version-compare.php
10+
///
11+
/// Returns `orderedAscending` if the lhs version is older than the rhs
12+
/// Returns `orderedSame` if the lhs version is the same as the rhs
13+
/// Returns `orderedDescending` if the lhs version is newer than the rhs
14+
///
15+
static func compare(_ lhs: String, _ rhs: String) -> ComparisonResult {
16+
17+
/// Replace _ - and + with a .
18+
///
19+
func replaceUnderscoreDashAndPlusWithDot(_ string: String) -> String {
20+
string.replacingOccurrences(of: "([_\\-+]+)", with: ".", options: .regularExpression)
21+
}
22+
23+
/// Insert a . before and after any non number
24+
///
25+
func insertDotsBeforeAndAfterAnyNonNumber(_ string: String) -> String {
26+
string.replacingOccurrences(of: "([^0-9.]+)", with: ".$1.", options: .regularExpression)
27+
}
28+
29+
/// Score and compare two string components
30+
///
31+
func compareStringComponents(_ lhs: String, _ rhs: String) -> ComparisonResult {
32+
/// Score each component
33+
let lhsScore = VersionComponentScore(from: lhs)
34+
let rhsScore = VersionComponentScore(from: rhs)
35+
36+
if lhsScore < rhsScore {
37+
return .orderedAscending
38+
}
39+
40+
if lhsScore > rhsScore {
41+
return .orderedDescending
42+
}
43+
44+
if lhsScore == .number && rhsScore == .number {
45+
let lhsAsNumber = NSNumber(value: Int(lhs) ?? 0)
46+
let rhsAsNumber = NSNumber(value: Int(rhs) ?? 0)
47+
48+
let comparisonResult = lhsAsNumber.compare(rhsAsNumber)
49+
if comparisonResult != .orderedSame {
50+
return comparisonResult
51+
}
52+
}
53+
54+
return .orderedSame
55+
}
56+
57+
/// Process the given string into version components
58+
///
59+
func versionComponents(of string: String) -> [String] {
60+
var stringToComponentize = replaceUnderscoreDashAndPlusWithDot(string)
61+
stringToComponentize = insertDotsBeforeAndAfterAnyNonNumber(stringToComponentize)
62+
return stringToComponentize.components(separatedBy: ".")
63+
}
64+
65+
let leftComponents = versionComponents(of: lhs)
66+
let rightComponents = versionComponents(of: rhs)
67+
68+
let maxComponents = max(leftComponents.count, rightComponents.count)
69+
70+
for index in 0..<maxComponents {
71+
/// Treat missing components (e.g. 1.2 being compared to 1.1.3 as "0", i.e. 1.2.0
72+
let leftComponent = index < leftComponents.count ? leftComponents[index] : "0"
73+
let rightComponent = index < rightComponents.count ? rightComponents[index] : "0"
74+
75+
let comparisonResult = compareStringComponents(leftComponent, rightComponent)
76+
if comparisonResult != .orderedSame {
77+
return comparisonResult
78+
}
79+
}
80+
81+
return .orderedSame
82+
}
83+
}
84+
85+
/// Defines the score (rank) of a component string within a version string.
86+
/// e.g. the "3" in 3.0.0beta3 should be treated as `.number`
87+
/// and the "beta" should be scored (ranked) lower as `.beta`.
88+
///
89+
/// The scores of components of version strings are used when comparing complete version strings
90+
/// to decide if one version is older, equal or newer than another.
91+
///
92+
/// Ranked per https://www.php.net/manual/en/function.version-compare.php
93+
///
94+
fileprivate enum VersionComponentScore: Comparable {
95+
case other
96+
case dev
97+
case alpha
98+
case beta
99+
case RC
100+
case number
101+
case patch
102+
}
103+
104+
fileprivate extension VersionComponentScore {
105+
init(from: String) {
106+
if from == "dev" {
107+
self = .dev
108+
return
109+
}
110+
if from == "alpha" || from == "a" {
111+
self = .alpha
112+
return
113+
}
114+
if from == "beta" || from == "b" {
115+
self = .beta
116+
return
117+
}
118+
if from == "RC" || from == "rc" {
119+
self = .RC
120+
return
121+
}
122+
let componentCharacterSet = CharacterSet(charactersIn: from)
123+
if componentCharacterSet.isSubset(of: .decimalDigits) {
124+
self = .number
125+
return
126+
}
127+
128+
if from == "pl" || from == "p" {
129+
self = .patch
130+
return
131+
}
132+
133+
self = .other
134+
}
135+
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/CardPresentPaymentsOnboardingUseCase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ private extension CardPresentPaymentsOnboardingUseCase {
197197
}
198198

199199
func isWCPayVersionSupported(plugin: SystemPlugin) -> Bool {
200-
plugin.version.compareAsVersion(to: Constants.minimumSupportedWCPayVersion) != .orderedAscending
200+
VersionHelpers.compare(plugin.version, Constants.minimumSupportedWCPayVersion) != .orderedAscending
201201
}
202202

203203
func isWCPayActivated(plugin: SystemPlugin) -> Bool {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@
494494
314DC4BF268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314DC4BE268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift */; };
495495
314DC4C1268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314DC4C0268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift */; };
496496
314DC4C3268D2F1000444C9E /* MockAppSettingsStoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314DC4C2268D2F1000444C9E /* MockAppSettingsStoresManager.swift */; };
497+
31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31579027273EE2B1008CA3AF /* VersionHelpers.swift */; };
497498
31595CAD25E966380033F0FF /* ConnectedReaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31595CAC25E966380033F0FF /* ConnectedReaderTableViewCell.xib */; };
498499
315E14F42698DA24000AD5FF /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 315E14F32698DA24000AD5FF /* PassKit.framework */; };
499500
316837DA25CCA90C00E36B2F /* OrderStatusListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316837D925CCA90C00E36B2F /* OrderStatusListDataSource.swift */; };
@@ -522,7 +523,7 @@
522523
31F21B02263C8E150035B50A /* CardReaderSettingsSearchingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F21B01263C8E150035B50A /* CardReaderSettingsSearchingViewModelTests.swift */; };
523524
31F21B5A263CB41A0035B50A /* MockCardPresentPaymentsStoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F21B59263CB41A0035B50A /* MockCardPresentPaymentsStoresManager.swift */; };
524525
31F21B60263CB78A0035B50A /* MockCardReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F21B5F263CB78A0035B50A /* MockCardReader.swift */; };
525-
31F635DC273AF0B100E14F10 /* StringHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F635DB273AF0B100E14F10 /* StringHelpersTests.swift */; };
526+
31F635DC273AF0B100E14F10 /* VersionHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F635DB273AF0B100E14F10 /* VersionHelpersTests.swift */; };
526527
31F92DE125E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F92DE025E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift */; };
527528
31FE28C225E6D338003519F2 /* LearnMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FE28C125E6D338003519F2 /* LearnMoreTableViewCell.swift */; };
528529
31FE28C825E6D384003519F2 /* LearnMoreTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 31FE28C725E6D384003519F2 /* LearnMoreTableViewCell.xib */; };
@@ -1958,6 +1959,7 @@
19581959
314DC4BE268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderSettingsKnownReaderStorage.swift; sourceTree = "<group>"; };
19591960
314DC4C0268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderSettingsKnownReadersStorageTests.swift; sourceTree = "<group>"; };
19601961
314DC4C2268D2F1000444C9E /* MockAppSettingsStoresManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppSettingsStoresManager.swift; sourceTree = "<group>"; };
1962+
31579027273EE2B1008CA3AF /* VersionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionHelpers.swift; sourceTree = "<group>"; };
19611963
31595CAC25E966380033F0FF /* ConnectedReaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConnectedReaderTableViewCell.xib; sourceTree = "<group>"; };
19621964
315E14F32698DA24000AD5FF /* PassKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PassKit.framework; path = System/Library/Frameworks/PassKit.framework; sourceTree = SDKROOT; };
19631965
316837D925CCA90C00E36B2F /* OrderStatusListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatusListDataSource.swift; sourceTree = "<group>"; };
@@ -1986,7 +1988,7 @@
19861988
31F21B01263C8E150035B50A /* CardReaderSettingsSearchingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderSettingsSearchingViewModelTests.swift; sourceTree = "<group>"; };
19871989
31F21B59263CB41A0035B50A /* MockCardPresentPaymentsStoresManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardPresentPaymentsStoresManager.swift; sourceTree = "<group>"; };
19881990
31F21B5F263CB78A0035B50A /* MockCardReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardReader.swift; sourceTree = "<group>"; };
1989-
31F635DB273AF0B100E14F10 /* StringHelpersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringHelpersTests.swift; sourceTree = "<group>"; };
1991+
31F635DB273AF0B100E14F10 /* VersionHelpersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionHelpersTests.swift; sourceTree = "<group>"; };
19901992
31F92DE025E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedReaderTableViewCell.swift; sourceTree = "<group>"; };
19911993
31FE28C125E6D338003519F2 /* LearnMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreTableViewCell.swift; sourceTree = "<group>"; };
19921994
31FE28C725E6D384003519F2 /* LearnMoreTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LearnMoreTableViewCell.xib; sourceTree = "<group>"; };
@@ -5242,6 +5244,7 @@
52425244
CE22709E2293052700C0626C /* WebviewHelper.swift */,
52435245
45D685FD23D0FB25005F87D0 /* Throttler.swift */,
52445246
262C921E26EEF8B100011F92 /* Binding.swift */,
5247+
31579027273EE2B1008CA3AF /* VersionHelpers.swift */,
52455248
);
52465249
path = Tools;
52475250
sourceTree = "<group>";
@@ -5441,7 +5444,7 @@
54415444
74F301592200EC0800931B9E /* NSDecimalNumberWooTests.swift */,
54425445
45DB706B26161F970064A6CF /* DecimalWooTests.swift */,
54435446
B541B2122189E7FD008FE7C1 /* ScannerWooTests.swift */,
5444-
31F635DB273AF0B100E14F10 /* StringHelpersTests.swift */,
5447+
31F635DB273AF0B100E14F10 /* VersionHelpersTests.swift */,
54455448
B55BC1F221A8790F0011A0C0 /* StringHTMLTests.swift */,
54465449
B5980A6421AC905C00EBF596 /* UIDeviceWooTests.swift */,
54475450
B57C745020F56EE900EEFC87 /* UITableViewCellHelpersTests.swift */,
@@ -7529,6 +7532,7 @@
75297532
B5D1AFC620BC7B7300DB0E8C /* StorePickerViewController.swift in Sources */,
75307533
02DD81FB242CAA400060E50B /* WordPressMediaLibraryPickerDataSource.swift in Sources */,
75317534
0240B3AC230A910C000A866C /* StoreStatsV4ChartAxisHelper.swift in Sources */,
7535+
31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */,
75327536
CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */,
75337537
E107FCE326C13A0D00BAF51B /* InPersonPaymentsSupportLink.swift in Sources */,
75347538
2662D90626E1571900E25611 /* ListSelector.swift in Sources */,
@@ -8365,7 +8369,7 @@
83658369
2667BFE92530ECE4008099D4 /* RefundProductsTotalViewModelTests.swift in Sources */,
83668370
D810F8F82639EDE900437C67 /* CardPresentPaymentsModalViewControllerTests.swift in Sources */,
83678371
02A275C623FE9EFC005C560F /* MockFeatureFlagService.swift in Sources */,
8368-
31F635DC273AF0B100E14F10 /* StringHelpersTests.swift in Sources */,
8372+
31F635DC273AF0B100E14F10 /* VersionHelpersTests.swift in Sources */,
83698373
FE3E427726A8545B00C596CE /* MockRoleEligibilityUseCase.swift in Sources */,
83708374
02F67FF525806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift in Sources */,
83718375
570AAB052472FACB00516C0C /* OrderDetailsDataSourceTests.swift in Sources */,

WooCommerce/WooCommerceTests/Extensions/StringHelpersTests.swift renamed to WooCommerce/WooCommerceTests/Extensions/VersionHelpersTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import XCTest
33

44
/// String+Helpers: Unit Tests
55
///
6-
final class StringHelpersTests: XCTestCase {
6+
final class VersionHelpersTests: XCTestCase {
77
func test_compare_as_version() {
88
let tests = [
99
VersionTestCase(foundVersion: "2.8", requiredMinimumVersion: "2", meetsMinimum: true),
@@ -44,7 +44,7 @@ final class StringHelpersTests: XCTestCase {
4444
]
4545

4646
for test in tests {
47-
let meetsMinimum = test.foundVersion.compareAsVersion(to: test.requiredMinimumVersion) != .orderedAscending
47+
let meetsMinimum = VersionHelpers.compare(test.foundVersion, test.requiredMinimumVersion) != .orderedAscending
4848
XCTAssertEqual(test.meetsMinimum, meetsMinimum)
4949
}
5050
}

0 commit comments

Comments
 (0)