Skip to content

Commit 213bf53

Browse files
authored
Merge pull request #720 from zonble/master
Add Roman numeral input support
2 parents 3602c82 + 20ef442 commit 213bf53

16 files changed

Lines changed: 610 additions & 69 deletions

File tree

.github/workflows/continuous-integration-workflow-xcode-latest.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ jobs:
6565
- name: Test ChineseNumbers
6666
run: swift test --enable-code-coverage
6767
working-directory: Packages/ChineseNumbers
68+
- name: Test RomanNumbers
69+
run: swift test --enable-code-coverage
70+
working-directory: Packages/RomanNumbers
6871
- name: Clean McBopomofo for testing
6972
run: xcodebuild -scheme McBopomofo -configuration Debug clean
7073
- name: Test McBopomofo

McBopomofo.xcodeproj/project.pbxproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
7474
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; };
7575
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; };
76+
D48F853E2EACDB6C00C2FDAB /* RomanNumbers in Frameworks */ = {isa = PBXBuildFile; productRef = D48F853D2EACDB6C00C2FDAB /* RomanNumbers */; };
7677
D4A13D5A27A59F0B003BE359 /* InputMethodController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A13D5927A59D5C003BE359 /* InputMethodController.swift */; };
7778
D4A8E43627A9E982002F7A07 /* KeyHandlerPlainBopomofoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A8E43527A9E982002F7A07 /* KeyHandlerPlainBopomofoTests.swift */; };
7879
D4C2A9872B4309D700113711 /* UTF8HelperTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = D4C2A9862B4309D700113711 /* UTF8HelperTest.mm */; };
@@ -215,6 +216,7 @@
215216
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
216217
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = "<group>"; };
217218
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = "<group>"; };
219+
D48F853C2EACCDE900C2FDAB /* RomanNumbers */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = RomanNumbers; path = Packages/RomanNumbers; sourceTree = "<group>"; };
218220
D495583A27A5C6C4006ADE1C /* LanguageModelManager+Privates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LanguageModelManager+Privates.h"; sourceTree = "<group>"; };
219221
D4A13D5927A59D5C003BE359 /* InputMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMethodController.swift; sourceTree = "<group>"; };
220222
D4A8E43527A9E982002F7A07 /* KeyHandlerPlainBopomofoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandlerPlainBopomofoTests.swift; sourceTree = "<group>"; };
@@ -253,6 +255,7 @@
253255
D427F7A927905E90004A2160 /* TooltipUI in Frameworks */,
254256
D4451AC92E688C6B00E8F5AB /* SystemCharacterInfo in Frameworks */,
255257
D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */,
258+
D48F853E2EACDB6C00C2FDAB /* RomanNumbers in Frameworks */,
256259
D4E7917A2B52CDE500676A68 /* ChineseNumbers in Frameworks */,
257260
D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */,
258261
D4C9CAB127AAC9690058DFEA /* NSStringUtils in Frameworks */,
@@ -459,6 +462,7 @@
459462
D4E5EEE22E68A9B30068BC58 /* SystemCharacterInfo */,
460463
D41B626A2B86EAD400583148 /* BopomofoBraille */,
461464
D4E791782B52CDCF00676A68 /* ChineseNumbers */,
465+
D48F853C2EACCDE900C2FDAB /* RomanNumbers */,
462466
D427F768278C9D0D004A2160 /* CandidateUI */,
463467
D427F7A727905E43004A2160 /* TooltipUI */,
464468
D427F7AC27907B7E004A2160 /* NotifierUI */,
@@ -539,6 +543,7 @@
539543
D4E791792B52CDE500676A68 /* ChineseNumbers */,
540544
D41B626B2B86EAE900583148 /* BopomofoBraille */,
541545
D4451AC82E688C6B00E8F5AB /* SystemCharacterInfo */,
546+
D48F853D2EACDB6C00C2FDAB /* RomanNumbers */,
542547
);
543548
productName = McBopomofo;
544549
productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */;
@@ -1467,6 +1472,10 @@
14671472
isa = XCSwiftPackageProductDependency;
14681473
productName = FSEventStreamHelper;
14691474
};
1475+
D48F853D2EACDB6C00C2FDAB /* RomanNumbers */ = {
1476+
isa = XCSwiftPackageProductDependency;
1477+
productName = RomanNumbers;
1478+
};
14701479
D4C9CAB027AAC9690058DFEA /* NSStringUtils */ = {
14711480
isa = XCSwiftPackageProductDependency;
14721481
productName = NSStringUtils;

Packages/ChineseNumbers/Tests/ChineseNumbersTests/ChineseNumbersTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
// Copyright (c) 2022 and onwards The McBopomofo Authors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person
4+
// obtaining a copy of this software and associated documentation
5+
// files (the "Software"), to deal in the Software without
6+
// restriction, including without limitation the rights to use,
7+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the
9+
// Software is furnished to do so, subject to the following
10+
// conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be
13+
// included in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
// OTHER DEALINGS IN THE SOFTWARE.
23+
124
import XCTest
225
@testable import ChineseNumbers
326

Packages/ChineseNumbers/Tests/ChineseNumbersTests/SuzhouTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
// Copyright (c) 2022 and onwards The McBopomofo Authors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person
4+
// obtaining a copy of this software and associated documentation
5+
// files (the "Software"), to deal in the Software without
6+
// restriction, including without limitation the rights to use,
7+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the
9+
// Software is furnished to do so, subject to the following
10+
// conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be
13+
// included in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
// OTHER DEALINGS IN THE SOFTWARE.
23+
124
import XCTest
225

326
@testable import ChineseNumbers

Packages/RomanNumbers/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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: "RomanNumbers",
8+
products: [
9+
// Products define the executables and libraries a package produces, making them visible to other packages.
10+
.library(
11+
name: "RomanNumbers",
12+
targets: ["RomanNumbers"]
13+
),
14+
],
15+
targets: [
16+
// Targets are the basic building blocks of a package, defining a module or a test suite.
17+
// Targets can depend on other targets in this package and products from dependencies.
18+
.target(
19+
name: "RomanNumbers"
20+
),
21+
.testTarget(
22+
name: "RomanNumbersTests",
23+
dependencies: ["RomanNumbers"]
24+
),
25+
]
26+
)

Packages/RomanNumbers/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# RomanNumbers
2+
3+
RomanNumbers is a Swift package that converts decimal numbers into Roman
4+
numerals. It powers parts of the McBopomofo input method project where formatted
5+
Roman numerals are needed in Swift and Objective-C contexts.
6+
7+
## Features
8+
9+
- Supports integer values from 0 through 3999
10+
- Offers three output styles via `RomanNumbersStyle`: `alphabets`,
11+
`fullWidthUpper` (Unicode U+2160–U+216F), and `fullWidthLower` (Unicode
12+
U+2170–U+217F)
13+
- Accepts either `Int` input or decimal text strings
14+
- Throws descriptive errors (`RomanNumbersErrors`) when the source value is out of range or invalid
15+
- Marked with `@objc` so the conversion APIs are reachable from Objective-C code
16+
17+
## Installation
18+
19+
Add RomanNumbers to the `dependencies` section of your `Package.swift`:
20+
21+
```swift
22+
.dependencies([
23+
.package(path: "Packages/RomanNumbers")
24+
])
25+
```
26+
27+
Then link the library from a target:
28+
29+
```swift
30+
.target(
31+
name: "YourTarget",
32+
dependencies: [
33+
.product(name: "RomanNumbers", package: "RomanNumbers")
34+
]
35+
)
36+
```
37+
38+
If you vend the package from another repository, replace the `.package` path
39+
with the appropriate `.package(url: "...", from: "...")` declaration.
40+
41+
## Usage
42+
43+
```swift
44+
import RomanNumbers
45+
46+
let number = 2025
47+
let standard = try RomanNumbers.convert(input: number)
48+
let upperFullWidth = try RomanNumbers.convert(input: number, style: .fullWidthUpper)
49+
let lowerFromText = try RomanNumbers.convert(string: "3999", style: .fullWidthLower)
50+
51+
print(standard) // "MMXXV"
52+
print(upperFullWidth) // Unicode Roman numeral letters in the U+2160 block
53+
print(lowerFromText) // Unicode Roman numeral letters in the U+2170 block
54+
```
55+
56+
## Error Handling
57+
58+
`RomanNumbers.convert` throws values of `RomanNumbersErrors`:
59+
60+
- `tooLarge`: the input exceeds 3999
61+
- `tooSmall`: the input is negative
62+
- `invalidInput`: the string argument cannot be parsed as an integer
63+
64+
These errors conform to `LocalizedError`, providing human-readable descriptions
65+
suitable for UI presentation.
66+
67+
## Testing
68+
69+
Run the package tests with:
70+
71+
```bash
72+
swift test
73+
```
74+
75+
The suite exercises every conversion style and covers edge cases for the
76+
supported numeric range.
77+
78+
## License
79+
80+
RomanNumbers ships as part of McBopomofo. Refer to the repository's root
81+
`LICENSE` file for licensing terms.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) 2022 and onwards The McBopomofo Authors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person
4+
// obtaining a copy of this software and associated documentation
5+
// files (the "Software"), to deal in the Software without
6+
// restriction, including without limitation the rights to use,
7+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
// copies of the Software, and to permit persons to whom the
9+
// Software is furnished to do so, subject to the following
10+
// conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be
13+
// included in all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
// OTHER DEALINGS IN THE SOFTWARE.
23+
24+
import Foundation
25+
26+
@objc
27+
public enum RomanNumbersErrors: Int, Error, LocalizedError {
28+
case tooLarge
29+
case tooSmall
30+
case invalidInput
31+
32+
public var errorDescription: String? {
33+
switch self {
34+
case .tooLarge:
35+
"Cannot be larger than 3999"
36+
case .tooSmall:
37+
"Cannot be less than 0"
38+
case .invalidInput:
39+
"Input is not a valid integer"
40+
}
41+
}
42+
}
43+
44+
@objc public enum RomanNumbersStyle: Int {
45+
case alphabets
46+
case fullWidthUpper
47+
case fullWidthLower
48+
}
49+
50+
struct DigitsMap {
51+
let digits: [String]
52+
let tens: [String]
53+
let hundreds: [String]
54+
let thousands: [String]
55+
}
56+
57+
extension RomanNumbersStyle {
58+
var digitsMap: DigitsMap {
59+
switch self {
60+
case .alphabets:
61+
DigitsMap(
62+
digits: ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"],
63+
tens: ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"],
64+
hundreds: ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"],
65+
thousands: ["", "M", "MM", "MMM"])
66+
case .fullWidthUpper:
67+
DigitsMap(
68+
digits: ["", "", "", "", "", "", "", "", "", ""],
69+
tens: ["", "", "ⅩⅩ", "ⅩⅩⅩ", "ⅩⅬ", "", "ⅬⅩ", "ⅬⅩⅩ", "ⅬⅩⅩⅩ", "ⅩⅭ"],
70+
hundreds: ["", "", "ⅭⅭ", "ⅭⅭⅭ", "ⅭⅮ", "", "ⅮⅭ", "ⅮⅭⅭ", "ⅮⅭⅭⅭ", "ⅭⅯ"],
71+
thousands: ["", "", "ⅯⅯ", "ⅯⅯⅯ"])
72+
case .fullWidthLower:
73+
DigitsMap(
74+
digits: ["", "", "", "", "", "", "", "", "", ""],
75+
tens: ["", "", "ⅹⅹ", "ⅹⅹⅹ", "ⅹⅼ", "", "ⅼⅹ", "ⅼⅹⅹ", "ⅼⅹⅹⅹ", "ⅹⅽ"],
76+
hundreds: ["", "", "ⅽⅽ", "ⅽⅽⅽ", "ⅽⅾ", "", "ⅾⅽ", "ⅾⅽⅽ", "ⅾⅽⅽⅽ", "ⅽⅿ"],
77+
thousands: ["", "", "ⅿⅿ", "ⅿⅿⅿ"])
78+
}
79+
}
80+
}
81+
82+
@objc
83+
public class RomanNumbers: NSObject {
84+
@objc(convertWithInt:style:error:)
85+
public static func convert(input: Int, style: RomanNumbersStyle = .alphabets) throws -> String {
86+
if input > 3999 {
87+
throw RomanNumbersErrors.tooLarge
88+
}
89+
if input < 0 {
90+
throw RomanNumbersErrors.tooSmall
91+
}
92+
93+
if style == .fullWidthUpper {
94+
switch input {
95+
case 11:
96+
return ""
97+
case 12:
98+
return ""
99+
default:
100+
break
101+
}
102+
}
103+
104+
if style == .fullWidthLower {
105+
switch input {
106+
case 11:
107+
return ""
108+
case 12:
109+
return ""
110+
default:
111+
break
112+
}
113+
114+
}
115+
116+
let thou = input / 1000
117+
let hund = (input % 1000) / 100
118+
let ten = (input % 100) / 10
119+
let digit = input % 10
120+
121+
let map = style.digitsMap
122+
123+
let result = map.thousands[thou] + map.hundreds[hund] + map.tens[ten] + map.digits[digit]
124+
return result
125+
}
126+
127+
@objc(convertWithString:style:error:)
128+
public static func convert(string: String, style: RomanNumbersStyle = .alphabets) throws
129+
-> String
130+
{
131+
guard let number = Int(string) else {
132+
throw RomanNumbersErrors.invalidInput
133+
}
134+
return try convert(input: number, style: style)
135+
}
136+
}

0 commit comments

Comments
 (0)