├── .swift-version ├── .swiftlint.yml ├── codecov.yml ├── RomanNumeralKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── kyle.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── RomanNumeralKit.xcscheme ├── Sources └── RomanNumeralKit │ ├── RomanNumeralKit.h │ ├── Info.plist │ ├── Numeric System │ ├── Subtractive Notation │ │ ├── RomanNumeralConvertible.swift │ │ ├── SubtractiveRomanNumeralSymbolConvertible.swift │ │ ├── SubtractiveRomanNumeralSymbolsConvertible.swift │ │ ├── SubtractiveRomanNumeralSymbol.swift │ │ └── RomanNumeral.swift │ ├── Additive Notation │ │ ├── AdditiveRomanNumeralConvertible.swift │ │ ├── AdditiveRomanNumeralSymbolConvertible.swift │ │ ├── AdditiveRomanNumeralSymbolsConvertible.swift │ │ ├── AdditiveRomanNumeralSymbolCondenser.swift │ │ └── AdditiveRomanNumeral.swift │ ├── RomanNumeralSymbolError.swift │ ├── RomanNumeralSymbolConvertible.swift │ ├── RomanNumeralSymbolsConvertible.swift │ ├── RomanNumeralError.swift │ ├── RomanNumeralTallyMark.swift │ ├── RomanNumeralArithmeticError.swift │ ├── RomanNumeralSymbolProtocol.swift │ ├── RomanNumeralTallyMarkGroup.swift │ ├── RomanNumeralProtocol.swift │ └── RomanNumeralSymbol.swift │ └── Extensions │ └── Foundation │ ├── DateComponents+RomanNumeral.swift │ ├── Int+RomanNumeral.swift │ ├── String+RomanNumeral.swift │ ├── Sequence+Sorting.swift │ └── Calendar+RomanNumeral.swift ├── CHANGELOG.md ├── RomanNumeralKit.podspec ├── Tests └── RomanNumeralKitTests │ ├── Info.plist │ ├── Numeric System │ ├── RomanNumeralErrorTests.swift │ ├── RomanNumeralArithmeticErrorTests.swift │ ├── RomanNumeralSymbolErrorTests.swift │ ├── RomanNumeralTallyMarkGroupTests.swift │ ├── RomanNumeralSymbolTests.swift │ └── Subtractive Notation │ │ └── SubtractiveRomanNumeralSymbolTests.swift │ └── Extensions │ └── Foundation │ ├── IntTests+RomanNumeral.swift │ ├── StringTests+RomanNumeral.swift │ ├── DateComponentsTests+RomanNumeral.swift │ └── CalendarTests+RomanNumeral.swift ├── LICENSE ├── Package.swift ├── .gitignore ├── .swiftformat ├── .travis.yml └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.1 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - todo -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/**/*" 3 | - "Sources/RomanNumeralKit/Numeric System/RomanNumerals.swift" -------------------------------------------------------------------------------- /RomanNumeralKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RomanNumeralKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/RomanNumeralKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralKit.h 3 | // RomanNumeralKit 4 | // 5 | // Created by Kyle Hughes on 4/16/18. 6 | // Copyright © 2018 Kyle Hughes. All rights reserved. 7 | // 8 | 9 | #import // `FOUNDATION_EXPORT` 10 | 11 | //! Project version number for RomanNumeralKit. 12 | FOUNDATION_EXPORT double RomanNumeralKitVersionNumber; 13 | 14 | //! Project version string for RomanNumeralKit. 15 | FOUNDATION_EXPORT const unsigned char RomanNumeralKitVersionString[]; 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.2.1] - 2021-06-24 6 | 7 | - Ignored `Info.plist` files in Swift package definition. 8 | 9 | ## [1.2.0] - 2021-03-13 10 | 11 | ### Changed 12 | 13 | - Removed development dependencies from Swift Package definition. 14 | 15 | ## [1.1.0] - 2020-09-05 16 | 17 | ### Changed 18 | 19 | - Improved performance of arithmetic functions. 20 | - Removed `dynamic` specifier from library product. 21 | 22 | ## [1.0.0] - 2019-08-28 23 | 24 | ### Added 25 | 26 | - Initial release of RomanNumeralKit. 27 | -------------------------------------------------------------------------------- /RomanNumeralKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "RomanNumeralKit" 3 | s.version = "1.2.1" 4 | s.license = "MIT" 5 | s.summary = "First-class Roman numeral support for Swift." 6 | s.homepage = "https://github.com/kylehughes/RomanNumeralKit" 7 | s.authors = "Kyle Hughes" 8 | s.source = { :git => "https://github.com/kylehughes/RomanNumeralKit.git", :tag => s.version } 9 | 10 | s.ios.deployment_target = "10.0" 11 | s.osx.deployment_target = "10.12" 12 | s.tvos.deployment_target = "10.0" 13 | s.watchos.deployment_target = "3.0" 14 | 15 | s.swift_version = "5.0" 16 | 17 | s.source_files = "Sources/RomanNumeralKit/**/*.swift" 18 | end 19 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.2.1 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RomanNumeralKit.xcodeproj/xcuserdata/kyle.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RomanNumeralKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | RomanNumeralKit_macOS.xcscheme 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 27667F9F2084B37D00AB8A66 21 | 22 | primary 23 | 24 | 25 | 27667FA82084B37D00AB8A66 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | RomanNumeralKit 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 1.2.1 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Kyle Hughes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "RomanNumeralKit", 7 | platforms: [ 8 | .macOS(.v10_12), 9 | .iOS(.v10), 10 | .tvOS(.v10), 11 | .watchOS(.v3), 12 | ], 13 | products: [ 14 | .library( 15 | name: "RomanNumeralKit", 16 | targets: [ 17 | "RomanNumeralKit", 18 | ] 19 | ), 20 | ], 21 | dependencies: [ 22 | ], 23 | targets: [ 24 | .target( 25 | name: "RomanNumeralKit", 26 | dependencies: [ 27 | ], 28 | exclude: [ 29 | "Info.plist", 30 | ] 31 | ), 32 | .testTarget( 33 | name: "RomanNumeralKitTests", 34 | dependencies: [ 35 | "RomanNumeralKit", 36 | ], 37 | exclude: [ 38 | "Info.plist", 39 | ] 40 | ), 41 | ], 42 | swiftLanguageVersions: [ 43 | .v5, 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /RomanNumeralKit.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // ___PRODUCTNAME___ 9 | // 10 | // Copyright © ___YEAR___ ___ORGANIZATIONNAME___. 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | // 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/ 5 | DerivedData/ 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata/ 17 | 18 | ## Other 19 | *.moved-aside 20 | *.xccheckout 21 | *.xcscmblueprint 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | *.dSYM.zip 27 | *.dSYM 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | # Packages/ 37 | # Package.pins 38 | # Package.resolved 39 | .swiftpm/ 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots/**/*.png 67 | fastlane/test_output -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Subtractive Notation/RomanNumeralConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralConvertible.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a Roman numeral using subtractive notation. 28 | */ 29 | public protocol RomanNumeralConvertible { 30 | // MARK: Instance Interface 31 | 32 | /// The Roman numeral representation of this instance using subtractive notation. 33 | var romanNumeral: RomanNumeral? { get } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Additive Notation/AdditiveRomanNumeralConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditiveRomanNumeralConvertible.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a Roman numeral using additive notation. 28 | */ 29 | public protocol AdditiveRomanNumeralConvertible { 30 | // MARK: Instance Interface 31 | 32 | /// The Roman numeral representation of this instance using additive notation. 33 | var additiveRomanNumeral: AdditiveRomanNumeral? { get } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Extensions/Foundation/DateComponents+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateComponents+RomanNumeral.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | extension DateComponents { 29 | // MARK: Public Instance Interface 30 | 31 | /// The year component, if present, represented as a Roman numeral. 32 | public var yearAsRomanNumeral: RomanNumeral? { 33 | guard let year = year else { 34 | return nil 35 | } 36 | 37 | return try? RomanNumeral(from: year) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Extensions/Foundation/Int+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+RomanNumeral.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | // MARK: - AdditiveRomanNumeralConvertible Extension 27 | 28 | extension Int: AdditiveRomanNumeralConvertible { 29 | // MARK: Public Instance Interface 30 | 31 | public var additiveRomanNumeral: AdditiveRomanNumeral? { 32 | try? AdditiveRomanNumeral(from: self) 33 | } 34 | } 35 | 36 | // MARK: - RomanNumeralConvertible Extension 37 | 38 | extension Int: RomanNumeralConvertible { 39 | // MARK: Public Instance Interface 40 | 41 | public var romanNumeral: RomanNumeral? { 42 | try? RomanNumeral(from: self) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Extensions/Foundation/String+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+RomanNumeral.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | // MARK: - AdditiveRomanNumeralConvertible Extension 27 | 28 | extension String: AdditiveRomanNumeralConvertible { 29 | // MARK: Public Instance Interface 30 | 31 | public var additiveRomanNumeral: AdditiveRomanNumeral? { 32 | try? AdditiveRomanNumeral(from: self) 33 | } 34 | } 35 | 36 | // MARK: - RomanNumeralConvertible Extension 37 | 38 | extension String: RomanNumeralConvertible { 39 | // MARK: Public Instance Interface 40 | 41 | public var romanNumeral: RomanNumeral? { 42 | try? RomanNumeral(from: self) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Extensions/Foundation/Sequence+Sorting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence+Sorting.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | extension Sequence { 27 | // MARK: Internal Instance Interface 28 | 29 | internal func isSorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Bool { 30 | var iterator = makeIterator() 31 | 32 | guard var previous = iterator.next() else { 33 | return true 34 | } 35 | 36 | while let current = iterator.next() { 37 | guard try areInIncreasingOrder(previous, current) else { 38 | return false 39 | } 40 | 41 | previous = current 42 | } 43 | 44 | return true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Numeric System/RomanNumeralErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralErrorTests.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class RomanNumeralErrorTests: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | } 41 | 42 | // MARK: - LocalizedError Tests 43 | 44 | extension RomanNumeralErrorTests { 45 | func test_errorDescription_present() { 46 | for error in RomanNumeralError.allCases { 47 | XCTAssertNotNil(error.errorDescription) 48 | XCTAssertNotEqual(error.errorDescription, "") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralSymbolError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbolError.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | /** 29 | Errors related to Roman numeral symbols. 30 | */ 31 | public enum RomanNumeralSymbolError: Error { 32 | /// The given `String` is not recognized as a valid Roman numeral symbol. 33 | case unrecognizedString(string: String) 34 | } 35 | 36 | // MARK: - LocalizedError Extension 37 | 38 | extension RomanNumeralSymbolError: LocalizedError { 39 | // MARK: Public Instance Interface 40 | 41 | public var errorDescription: String? { 42 | switch self { 43 | case let .unrecognizedString(string): 44 | return "\"\(string)\" is not recognized as a valid Roman numeral symbol." 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralSymbolConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbolConvertible.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a subtractive Roman numeral symbol using subtractive notation. 28 | */ 29 | public protocol RomanNumeralSymbolConvertible: RomanNumeralSymbolsConvertible { 30 | // MARK: Instance Interface 31 | 32 | /// The Roman numeral symbol representation of this instance using subtractive notation. 33 | var romanNumeralSymbol: RomanNumeralSymbol { get } 34 | } 35 | 36 | // MARK: - RomanNumeralSymbolsConvertible Extension 37 | 38 | extension RomanNumeralSymbolConvertible { 39 | // MARK: Public Instance Interface 40 | 41 | public var romanNumeralSymbols: [RomanNumeralSymbol] { 42 | [romanNumeralSymbol] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Numeric System/RomanNumeralArithmeticErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralArithmeticErrorTests.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class RomanNumeralArithmeticErrorTests: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | } 41 | 42 | // MARK: - LocalizedError Tests 43 | 44 | extension RomanNumeralArithmeticErrorTests { 45 | func test_errorDescription_present() { 46 | for error in RomanNumeralArithmeticError.allCases { 47 | XCTAssertNotNil(error.errorDescription) 48 | XCTAssertNotEqual(error.errorDescription, "") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Numeric System/RomanNumeralSymbolErrorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbolErrorTests.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class RomanNumeralSymbolErrorTests: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | } 41 | 42 | // MARK: - LocalizedError Tests 43 | 44 | extension RomanNumeralSymbolErrorTests { 45 | func test_errorDescription_present() { 46 | let unrecognizedStringError = RomanNumeralSymbolError.unrecognizedString(string: "R") 47 | XCTAssertNotNil(unrecognizedStringError.errorDescription) 48 | XCTAssertNotEqual(unrecognizedStringError.errorDescription, "") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Additive Notation/AdditiveRomanNumeralSymbolConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditiveRomanNumeralSymbolConvertible.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a Roman numeral symbol using additive notation. 28 | */ 29 | public protocol AdditiveRomanNumeralSymbolConvertible: AdditiveRomanNumeralSymbolsConvertible { 30 | // MARK: Instance Interface 31 | 32 | /// The Roman numeral symbol representation of this instance using additive notation. 33 | var additiveRomanNumeralSymbol: RomanNumeralSymbol { get } 34 | } 35 | 36 | // MARK: - AdditiveRomanNumeralSymbolsConvertible Extension 37 | 38 | extension AdditiveRomanNumeralSymbolConvertible { 39 | // MARK: Public Instance Interface 40 | 41 | public var additiveRomanNumeralSymbols: [RomanNumeralSymbol] { 42 | [additiveRomanNumeralSymbol] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Extensions/Foundation/Calendar+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calendar+RomanNumeral.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | extension Calendar { 29 | // MARK: Public Instance Interface 30 | 31 | /// The current year of the `Calendar` represented as a Roman numeral. 32 | public var currentYearAsRomanNumeral: RomanNumeral? { 33 | yearAsRomanNumeral(fromDate: Date()) 34 | } 35 | 36 | /** 37 | Returns the year of the given date as a Roman numeral. 38 | 39 | - Parameter date: The date whose year should be represented as a Roman numeral. 40 | - Returns: The year of the given date as a Roman numeral. 41 | */ 42 | public func yearAsRomanNumeral(fromDate date: Date) -> RomanNumeral? { 43 | let dateComponents = self.dateComponents([.year], from: date) 44 | 45 | return dateComponents.yearAsRomanNumeral 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # Format Options 2 | 3 | --allman false 4 | --binarygrouping 4,8 5 | --closingparen balanced 6 | --commas always 7 | --comments indent 8 | --decimalgrouping 3,6 9 | --elseposition same-line 10 | --empty void 11 | --exponentcase lowercase 12 | --exponentgrouping disabled 13 | --fractiongrouping disabled 14 | --header ignore 15 | --hexgrouping 4,8 16 | --hexliteralcase uppercase 17 | --ifdef indent 18 | --indent 4 19 | --indentcase false 20 | --importgrouping testable-bottom 21 | --linebreaks lf 22 | --octalgrouping 4,8 23 | --operatorfunc spaced 24 | --patternlet hoist 25 | --ranges spaced 26 | --self remove 27 | --semicolons inline 28 | --stripunusedargs always 29 | --trimwhitespace always 30 | --xcodeindentation disabled 31 | --wraparguments before-first 32 | --wrapcollections before-first 33 | 34 | # Rules 35 | 36 | --disable andOperator 37 | --enable anyObjectProtocol 38 | --enable blankLinesAroundMark 39 | --enable blankLinesAtEndOfScope 40 | --enable blankLinesAtStartOfScope 41 | --enable blankLinesBetweenScopes 42 | --enable consecutiveBlankLines 43 | --enable consecutiveSpaces 44 | --enable duplicateImports 45 | --enable emptyBraces 46 | --enable isEmpty 47 | --enable leadingDelimiters 48 | --enable linebreakAtEndOfFile 49 | --enable redundantBackticks 50 | --enable redundantBreak 51 | --enable redundantExtensionACL 52 | --enable redundantFileprivate 53 | --enable redundantGet 54 | --enable redundantInit 55 | --enable redundantLet 56 | --enable redundantLetError 57 | --enable redundantNilInit 58 | --enable redundantObjc 59 | --enable redundantParens 60 | --enable redundantPattern 61 | --enable redundantRawValues 62 | --enable redundantReturn 63 | --enable redundantVoidReturnType 64 | --enable spaceAroundBraces 65 | --enable spaceAroundBrackets 66 | --enable spaceAroundComments 67 | --enable spaceAroundGenerics 68 | --enable spaceAroundParens 69 | --enable spaceInsideBraces 70 | --enable spaceInsideBrackets 71 | --enable spaceInsideComments 72 | --enable spaceInsideGenerics 73 | --enable spaceInsideParens 74 | --enable specifiers 75 | --enable strongOutlets 76 | --enable strongifiedSelf 77 | --enable todos 78 | --enable typeSugar -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Additive Notation/AdditiveRomanNumeralSymbolsConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditiveRomanNumeralSymbolsConvertible.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a collection of Roman numeral symbols using additive notation. 28 | */ 29 | public protocol AdditiveRomanNumeralSymbolsConvertible: AdditiveRomanNumeralConvertible { 30 | // MARK: Instance Interface 31 | 32 | /// The representation of this instance as a collection of Roman numeral symbols using additive notation. 33 | var additiveRomanNumeralSymbols: [RomanNumeralSymbol] { get } 34 | } 35 | 36 | // MARK: - AdditiveRomanNumeralConvertible Extension 37 | 38 | extension AdditiveRomanNumeralSymbolsConvertible { 39 | // MARK: Public Instance Properties 40 | 41 | public var additiveRomanNumeral: AdditiveRomanNumeral? { 42 | try? AdditiveRomanNumeral(symbols: additiveRomanNumeralSymbols) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | os: osx 3 | osx_image: xcode11.2 4 | branches: 5 | only: 6 | - mainline 7 | 8 | cache: 9 | directories: 10 | # SwiftPM 11 | - .build 12 | 13 | env: 14 | global: 15 | - LC_CTYPE=en_US.UTF-8 16 | - LANG=en_US.UTF-8 17 | - FRAMEWORK_SCHEME=RomanNumeralKit 18 | matrix: 19 | - DESTINATION="OS=6.1,name=Apple Watch Series 5 - 44mm" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="NO" 20 | - DESTINATION="OS=5.3,name=Apple Watch Series 4 - 44mm" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="NO" 21 | - DESTINATION="OS=4.2,name=Apple Watch Series 3 - 42mm" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="NO" 22 | - DESTINATION="OS=3.2,name=Apple Watch - 38mm" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="NO" 23 | 24 | - DESTINATION="OS=13.2.2,name=iPhone 11" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 25 | - DESTINATION="OS=12.4,name=iPhone XS" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 26 | - DESTINATION="OS=11.4,name=iPhone X" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 27 | - DESTINATION="OS=10.3.1,name=iPhone 7" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 28 | 29 | - DESTINATION="OS=13.2,name=Apple TV 4K" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 30 | - DESTINATION="OS=12.4,name=Apple TV 4K" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 31 | - DESTINATION="OS=11.4,name=Apple TV 4K" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 32 | - DESTINATION="OS=10.2,name=Apple TV 1080p" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 33 | 34 | - DESTINATION="arch=x86_64" SCHEME="$FRAMEWORK_SCHEME" RUN_TESTS="YES" 35 | script: 36 | - set -o pipefail 37 | - xcodebuild -version 38 | - xcodebuild -showsdks 39 | - instruments -s devices 40 | 41 | # Build framework in Debug and run tests if specified 42 | - if [ $RUN_TESTS == "YES" ]; then 43 | xcodebuild -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO test | xcpretty -c; 44 | else 45 | xcodebuild -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty -c; 46 | fi 47 | 48 | after_success: 49 | - bash <(curl -s https://codecov.io/bash) -J 'RomanNumeralKit' -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Extensions/Foundation/IntTests+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntTests+AdditiveRomanNumeralConvertible.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class IntTestsPlusRomanNumeral: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | } 41 | 42 | // MARK: - AdditiveRomanNumeralConvertible Tests 43 | 44 | extension IntTestsPlusRomanNumeral { 45 | func test_additiveRomanNumeral() { 46 | XCTAssertEqual(1.additiveRomanNumeral, AdditiveRomanNumeral.minimum) 47 | XCTAssertEqual(3999.additiveRomanNumeral, AdditiveRomanNumeral.maximum) 48 | } 49 | } 50 | 51 | // MARK: - RomanNumeralConvertible Tests 52 | 53 | extension IntTestsPlusRomanNumeral { 54 | func test_romanNumeral() { 55 | XCTAssertEqual(1.romanNumeral, RomanNumeral.minimum) 56 | XCTAssertEqual(3999.romanNumeral, RomanNumeral.maximum) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Extensions/Foundation/StringTests+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringTests+RomanNumeralSymbol.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class StringTestsPlusRomanNumeralSymbol: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | } 41 | 42 | // MARK: - AdditiveRomanNumeralConvertible Extension 43 | 44 | extension StringTestsPlusRomanNumeralSymbol { 45 | func test_additiveRomanNumeral() { 46 | XCTAssertEqual("I".additiveRomanNumeral, AdditiveRomanNumeral.minimum) 47 | XCTAssertEqual("MMMDCCCCLXXXXVIIII".additiveRomanNumeral, AdditiveRomanNumeral.maximum) 48 | } 49 | } 50 | 51 | // MARK: - RomanNumeralConvertible 52 | 53 | extension StringTestsPlusRomanNumeralSymbol { 54 | func test_romanNumeral() { 55 | XCTAssertEqual("I".romanNumeral, RomanNumeral.minimum) 56 | XCTAssertEqual("MMMCMXCIX".romanNumeral, RomanNumeral.maximum) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Subtractive Notation/SubtractiveRomanNumeralSymbolConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubtractiveRomanNumeralSymbolConvertible.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a subtractive Roman numeral symbol using subtractive notation. 28 | 29 | This differs from `RomanNumeralSymbolConvertible` because this representation is using the non-standard 30 | `SubtractiveRomanNumeralSymbol` representation. 31 | */ 32 | public protocol SubtractiveRomanNumeralSymbolConvertible: SubtractiveRomanNumeralSymbolsConvertible { 33 | // MARK: Instance Interface 34 | 35 | /// The subtractive Roman numeral symbol representation of this instance using subtractive notation. 36 | var subtractiveRomanNumeralSymbol: SubtractiveRomanNumeralSymbol { get } 37 | } 38 | 39 | // MARK: - SubtractiveRomanNumeralSymbolsConvertible Extension 40 | 41 | extension SubtractiveRomanNumeralSymbolConvertible { 42 | // MARK: Public Instance Interface 43 | 44 | public var subtractiveRomanNumeralSymbols: [SubtractiveRomanNumeralSymbol] { 45 | [subtractiveRomanNumeralSymbol] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Subtractive Notation/SubtractiveRomanNumeralSymbolsConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubtractiveRomanNumeralSymbolConvertible.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a collection of subtractive Roman numeral symbols using subtractive notation. 28 | 29 | This differs from `RomanNumeralSymbolsConvertible` because this representation is using the non-standard 30 | `SubtractiveRomanNumeralSymbol` representation. 31 | */ 32 | public protocol SubtractiveRomanNumeralSymbolsConvertible: RomanNumeralConvertible { 33 | // MARK: Instance Interface 34 | 35 | /// The representation of this instance as a collection of subtractive Roman numeral symbols using subtractive 36 | /// notation. 37 | var subtractiveRomanNumeralSymbols: [SubtractiveRomanNumeralSymbol] { get } 38 | } 39 | 40 | // MARK: - RomanNumeralConvertible Extension 41 | 42 | extension SubtractiveRomanNumeralSymbolsConvertible { 43 | // MARK: Public Instance Interface 44 | 45 | public var romanNumeral: RomanNumeral? { 46 | try? RomanNumeral(subtractiveSymbols: subtractiveRomanNumeralSymbols) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralSymbolsConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbolsConvertible.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A type that can be represented as a collection of Roman numeral symbols using subtractive notation. 28 | */ 29 | public protocol RomanNumeralSymbolsConvertible: SubtractiveRomanNumeralSymbolsConvertible { 30 | // MARK: Instance Interface 31 | 32 | /// The representation of this instance as a collection of Roman numeral symbols using subtractive notation. 33 | var romanNumeralSymbols: [RomanNumeralSymbol] { get } 34 | } 35 | 36 | // MARK: - RomanNumeralConvertible Extension 37 | 38 | extension RomanNumeralSymbolsConvertible { 39 | // MARK: Public Instance Interface 40 | 41 | public var romanNumeral: RomanNumeral? { 42 | try? RomanNumeral(symbols: romanNumeralSymbols) 43 | } 44 | } 45 | 46 | // MARK: - SubtractiveRomanNumeralSymbolsConvertible Extension 47 | 48 | extension RomanNumeralSymbolsConvertible { 49 | // MARK: Public Instance Interface 50 | 51 | public var subtractiveRomanNumeralSymbols: [SubtractiveRomanNumeralSymbol] { 52 | AdditiveRomanNumeral.convert(toSymbolEquivalentSubtractiveSymbols: romanNumeralSymbols) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralError.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | /** 29 | Errors related to Roman numerals. 30 | 31 | For errors specific to Roman numeral symbols please see `RomanNumeralSymbolError`. 32 | */ 33 | public enum RomanNumeralError: Error, CaseIterable { 34 | /// The given symbols for the Roman numeral are out-of-order for the notation. 35 | case symbolsOutOfOrder 36 | 37 | /// A Roman numeral's value cannot be greater than its notation's maximum value. 38 | case valueGreaterThanMaximum 39 | 40 | /// A Roman numeral's value cannot be less than its notation's minimum value. 41 | case valueLessThanMinimum 42 | } 43 | 44 | // MARK: - LocalizedError Extension 45 | 46 | extension RomanNumeralError: LocalizedError { 47 | // MARK: Public Instance Interface 48 | 49 | public var errorDescription: String? { 50 | switch self { 51 | case .symbolsOutOfOrder: 52 | return "The given symbols for the Roman numeral are out-of-order for the notation." 53 | case .valueGreaterThanMaximum: 54 | return "A Roman numeral's value cannot be greater than its notation's maximum value." 55 | case .valueLessThanMinimum: 56 | return "A Roman numeral's value cannot be less than its notation's minimum value." 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralTallyMark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralTallyMark.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A tally mark used for counting. 28 | 29 | This is the atomic component that backs the value of each Roman numeral symbol. Tally marks are typically counted in 30 | groups (see `RomanNumeralTallyMarkGroup`). 31 | 32 | The tally mark should not be confused with the Roman numeral symbol `I`, although both share the same numeric value. 33 | The tally mark is represented using the ASCII "vertical bar" character. 34 | 35 | - SeeAlso: https://theasciicode.com.ar/ascii-printable-characters/vertical-bar-vbar-vertical-line-vertical-slash-ascii-code-124.html 36 | */ 37 | public struct RomanNumeralTallyMark: Equatable { 38 | // MARK: Public Initialization 39 | 40 | /** 41 | Creates a tally mark. 42 | */ 43 | public init() {} 44 | } 45 | 46 | // MARK: - CustomStringConvertible Extension 47 | 48 | extension RomanNumeralTallyMark: CustomStringConvertible { 49 | // MARK: Public Instance Interface 50 | 51 | public var description: String { 52 | "|" 53 | } 54 | } 55 | 56 | // MARK: - CustomDebugStringConvertible Extension 57 | 58 | extension RomanNumeralTallyMark: CustomDebugStringConvertible { 59 | // MARK: Public Instance Interface 60 | 61 | public var debugDescription: String { 62 | description 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Extensions/Foundation/DateComponentsTests+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateComponentsTests+RomanNumeral.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class DateComponentsTestsPlusRomanNumeral: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | 41 | // MARK: Tests 42 | 43 | func test_yearAsRomanNumeral_success() { 44 | // Given... 45 | 46 | let date = Date(timeIntervalSince1970: 1_012_737_600) // 2002-02-03 47 | let yearDateComponents = Calendar.current.dateComponents([.year], from: date) 48 | 49 | let expectedRomanNumeral = MMII 50 | 51 | // When... 52 | 53 | let actualRomanNumeral = yearDateComponents.yearAsRomanNumeral 54 | 55 | // Then... 56 | 57 | XCTAssertEqual(actualRomanNumeral, expectedRomanNumeral) 58 | } 59 | 60 | func test_yearAsRomanNumeral_noYear() { 61 | // Given... 62 | 63 | let date = Date(timeIntervalSince1970: 1_012_737_600) // 2002-02-03 64 | let yearDateComponents = Calendar.current.dateComponents([.month], from: date) 65 | 66 | // Then... 67 | 68 | XCTAssertNil(yearDateComponents.yearAsRomanNumeral) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Extensions/Foundation/CalendarTests+RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarTests+RomanNumeral.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class CalendarTestsPlusRomanNumeral: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() { 34 | super.setUp() 35 | } 36 | 37 | override func tearDown() { 38 | super.tearDown() 39 | } 40 | 41 | // MARK: Tests 42 | 43 | func test_currentYearAsRomanNumeral() { 44 | // Given... 45 | 46 | let currentYear = Calendar.current.component(.year, from: Date()) 47 | let expectedRomanNumeral = try? RomanNumeral(from: currentYear) 48 | 49 | // When... 50 | 51 | let actualRomanNumeral = Calendar.current.currentYearAsRomanNumeral 52 | 53 | // Then... 54 | 55 | XCTAssertNotNil(actualRomanNumeral) 56 | XCTAssertEqual(actualRomanNumeral, expectedRomanNumeral) 57 | } 58 | 59 | func test_yearAsRomanNumeral() { 60 | // Given... 61 | 62 | let date = Date(timeIntervalSince1970: 712_800_001) // 1992-08-03 63 | 64 | let expectedRomanNumeral = MCMXCII 65 | 66 | // When... 67 | 68 | let actualRomanNumeral = Calendar.current.yearAsRomanNumeral(fromDate: date) 69 | 70 | // Then... 71 | 72 | XCTAssertEqual(actualRomanNumeral, expectedRomanNumeral) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralArithmeticError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralArithmeticError.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | /** 29 | Errors related to Roman numerals arithmetic operations. 30 | */ 31 | public enum RomanNumeralArithmeticError: Error, CaseIterable { 32 | /// An unknown error occurred during the addition operation of two Roman numerals. 33 | case ambiguousAdditionError 34 | 35 | /// An unknown error occurred during the multiplication operation of two Roman numerals. 36 | case ambiguousMultiplicationError 37 | 38 | /// An unknown error occurred during the subtraction operation of two Roman numerals. 39 | case ambiguousSubtractionError 40 | 41 | /// Subtraction that will result in a negative value is not supported. 42 | case subtractionWhereRightValueIsGreaterThanLeftValue 43 | } 44 | 45 | // MARK: - LocalizedError Extension 46 | 47 | extension RomanNumeralArithmeticError: LocalizedError { 48 | // MARK: Public Instance Interface 49 | 50 | public var errorDescription: String? { 51 | switch self { 52 | case .ambiguousAdditionError: 53 | return "An unknown error occurred during the addition operation of two Roman numerals." 54 | case .ambiguousMultiplicationError: 55 | return "An unknown error occurred during the multiplication operation of two Roman numerals." 56 | case .ambiguousSubtractionError: 57 | return "An unknown error occurred during the subtraction operation of two Roman numerals." 58 | case .subtractionWhereRightValueIsGreaterThanLeftValue: 59 | return "Subtraction that will result in a negative value is not supported." 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralSymbolProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbolProtocol.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | Roman numeral symbols are a collection of letters from the Latin alphabet that are used to represent numbers in the 28 | numeric system of ancient Rome. Each symbol is shorthand for a specific number of tally marks. 29 | 30 | The backing values of the Roman numeral symbols, as found in their `rawValue`, are `RomanNumeralTallyMarkGroups` which 31 | are used to collect all of the corresponding `RomanNumeralTallyMark`s. Any representations of the symbols as Western 32 | Arabic numerals (e.g. `Int`) are conversions made by counting these tally marks. 33 | */ 34 | public protocol RomanNumeralSymbolProtocol: Comparable, 35 | CustomStringConvertible, 36 | CustomDebugStringConvertible, 37 | RawRepresentable where RawValue == RomanNumeralTallyMarkGroup { 38 | // MARK: Initialization 39 | 40 | /** 41 | Creates a Roman numeral symbol from a `String` value if there is a corresponding symbol for the `String`. 42 | 43 | - Parameter stringValue: The string representation of the desired Roman numeral symbol. 44 | - Throws: `RomanNumeralSymbolError.unrecognizedString` if the given string does not match a known Roman 45 | numeral symbol. 46 | */ 47 | init(from stringValue: String) throws 48 | 49 | // MARK: Static Interface 50 | 51 | /// All of the Roman numeral symbols, represented in ascending order by value. 52 | static var allSymbolsAscending: [Self] { get } 53 | 54 | /// All of the Roman numeral symbols, represented in descending order by value. 55 | static var allSymbolsDescending: [Self] { get } 56 | 57 | // MARK: Instance Interface 58 | 59 | /** 60 | The Roman numeral symbol whose value is next-lowest. 61 | 62 | For example, `V` is the lesser symbol to `X`, but `V` is not the lesser symbol to `L`. 63 | 64 | - Note: Both `I` and `nulla` will return `nil` because 0 is technically not a recognized value. 65 | */ 66 | var lesserSymbol: Self? { get } 67 | 68 | /// The `String` representation of the Roman numeral symbol. 69 | var stringValue: String { get } 70 | } 71 | 72 | // MARK: - Comparable Extension 73 | 74 | extension RomanNumeralSymbolProtocol { 75 | // MARK: Public Static Interface 76 | 77 | public static func < (lhs: Self, rhs: Self) -> Bool { 78 | lhs.rawValue.tallyMarks.count < rhs.rawValue.tallyMarks.count 79 | } 80 | } 81 | 82 | // MARK: - CustomDebugStringConvertible Extension 83 | 84 | extension RomanNumeralSymbolProtocol { 85 | // MARK: Public Instance Interface 86 | 87 | public var debugDescription: String { 88 | String(stringValue) 89 | } 90 | } 91 | 92 | // MARK: - CustomStringConvertible Extension 93 | 94 | extension RomanNumeralSymbolProtocol { 95 | // MARK: Public Instance Interface 96 | 97 | public var description: String { 98 | String(stringValue) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /RomanNumeralKit.xcodeproj/xcshareddata/xcschemes/RomanNumeralKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Additive Notation/AdditiveRomanNumeralSymbolCondenser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditiveRomanNumeralSymbolCondenser.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | internal struct AdditiveRomanNumeralSymbolCondenser { 27 | private(set) var countForI: Int 28 | private(set) var countForV: Int 29 | private(set) var countForX: Int 30 | private(set) var countForL: Int 31 | private(set) var countForC: Int 32 | private(set) var countForD: Int 33 | private(set) var countForM: Int 34 | 35 | // MARK: Internal Initialization 36 | 37 | internal init() { 38 | countForI = 0 39 | countForV = 0 40 | countForX = 0 41 | countForL = 0 42 | countForC = 0 43 | countForD = 0 44 | countForM = 0 45 | } 46 | 47 | // MARK: Internal Instance Interface 48 | 49 | internal mutating func combine(symbols: [RomanNumeralSymbol]) { 50 | symbols.forEach { record(symbol: $0) } 51 | } 52 | 53 | internal mutating func finalize() -> [RomanNumeralSymbol] { 54 | RomanNumeralSymbol 55 | .allSymbolsAscending 56 | .forEach { condenseAndRecord(symbol: $0) } 57 | 58 | return RomanNumeralSymbol 59 | .allSymbolsDescending 60 | .map { ($0, getCount(forSymbol: $0)) } 61 | .map(Array.init) 62 | .reduce([], +) 63 | } 64 | 65 | // MARK: Private Static Interface 66 | 67 | private static func condense(symbol: RomanNumeralSymbol, ofCount count: Int) -> [RomanNumeralSymbol] { 68 | let allSymbols = RomanNumeralSymbol.allSymbolsAscending 69 | 70 | guard let symbolIndex = allSymbols.firstIndex(of: symbol) else { 71 | return [] 72 | } 73 | 74 | let nextHighestSymbolIndex = symbolIndex + 1 75 | 76 | guard nextHighestSymbolIndex < allSymbols.count else { 77 | return Array(repeating: symbol, count: count) 78 | } 79 | 80 | let nextHighestSymbol = allSymbols[nextHighestSymbolIndex] 81 | let nextHighestSymbolAsCurrentSymbols = nextHighestSymbol.expandedIntoLesserSymbol 82 | let nextHighestSymbolQuantity = count / nextHighestSymbolAsCurrentSymbols.count 83 | let nextHighestSymbols = Array(repeating: nextHighestSymbol, count: nextHighestSymbolQuantity) 84 | 85 | let remainingSymbolQuanity = count % nextHighestSymbolAsCurrentSymbols.count 86 | let remainingSymbols = Array(repeating: symbol, count: remainingSymbolQuanity) 87 | 88 | return nextHighestSymbols + remainingSymbols 89 | } 90 | 91 | // MARK: Private Instance Interface 92 | 93 | private mutating func clearRecord(forSymbol symbol: RomanNumeralSymbol) { 94 | switch symbol { 95 | case .nulla: 96 | break 97 | case .I: 98 | countForI = 0 99 | case .V: 100 | countForV = 0 101 | case .X: 102 | countForX = 0 103 | case .L: 104 | countForL = 0 105 | case .C: 106 | countForC = 0 107 | case .D: 108 | countForD = 0 109 | case .M: 110 | countForM = 0 111 | } 112 | } 113 | 114 | private mutating func condenseAndRecord(symbol: RomanNumeralSymbol) { 115 | let count = getCount(forSymbol: symbol) 116 | let condensedSymbols = Self.condense(symbol: symbol, ofCount: count) 117 | clearRecord(forSymbol: symbol) 118 | condensedSymbols.forEach { record(symbol: $0) } 119 | } 120 | 121 | private mutating func getCount(forSymbol symbol: RomanNumeralSymbol) -> Int { 122 | switch symbol { 123 | case .nulla: 124 | return 0 125 | case .I: 126 | return countForI 127 | case .V: 128 | return countForV 129 | case .X: 130 | return countForX 131 | case .L: 132 | return countForL 133 | case .C: 134 | return countForC 135 | case .D: 136 | return countForD 137 | case .M: 138 | return countForM 139 | } 140 | } 141 | 142 | private mutating func record(symbol: RomanNumeralSymbol) { 143 | switch symbol { 144 | case .nulla: 145 | break 146 | case .I: 147 | countForI += 1 148 | case .V: 149 | countForV += 1 150 | case .X: 151 | countForX += 1 152 | case .L: 153 | countForL += 1 154 | case .C: 155 | countForC += 1 156 | case .D: 157 | countForD += 1 158 | case .M: 159 | countForM += 1 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralTallyMarkGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralTallyMarkGroup.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A collection of tally marks that serves as the backing value for a Roman numeral symbol. 28 | 29 | For example, `IIIII` contains five tally marks, so the value of the group is five. 30 | */ 31 | public struct RomanNumeralTallyMarkGroup: Equatable { 32 | // MARK: Public Static Properties 33 | 34 | /// A tally mark group with 0 tally marks. 35 | public static let nulla = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 0) 36 | 37 | /// A tally mark group with 1 tally mark. 38 | public static let one = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1) 39 | 40 | /// A tally mark group with 4 tally marks. 41 | public static let four = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 4) 42 | 43 | /// A tally mark group with 5 tally marks. 44 | public static let five = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 5) 45 | 46 | /// A tally mark group with 9 tally marks. 47 | public static let nine = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 9) 48 | 49 | /// A tally mark group with 10 tally marks. 50 | public static let ten = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 10) 51 | 52 | /// A tally mark group with 40 tally marks. 53 | public static let forty = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 40) 54 | 55 | /// A tally mark group with 50 tally marks. 56 | public static let fifty = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 50) 57 | 58 | /// A tally mark group with 90 tally marks. 59 | public static let ninety = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 90) 60 | 61 | /// A tally mark group with 100 tally marks. 62 | public static let oneHundred = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 100) 63 | 64 | /// A tally mark group with 400 tally marks. 65 | public static let fourHundred = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 400) 66 | 67 | /// A tally mark group with 500 tally marks. 68 | public static let fiveHundred = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 500) 69 | 70 | /// A tally mark group with 900 tally marks. 71 | public static let nineHundred = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 900) 72 | 73 | /// A tally mark group with 1000 tally marks. 74 | public static let oneThousand = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1000) 75 | 76 | // MARK: Public Instance Properties 77 | 78 | /// The tally marks that are in the group. The number of tally marks represents the value of the group. 79 | public let tallyMarks: [RomanNumeralTallyMark] 80 | 81 | // MARK: Public Initialization 82 | 83 | /** 84 | Creates a tally mark group that contains the given number of tally marks. 85 | 86 | - Parameter numberOfTallyMarks: The number of tally marks in the group. 87 | */ 88 | public init(numberOfTallyMarks: Int) { 89 | tallyMarks = Array(repeating: RomanNumeralTallyMark(), count: numberOfTallyMarks) 90 | } 91 | 92 | /** 93 | Creates a tally mark group that contains the given tally marks. 94 | 95 | - Parameter tallyMarks: The tally marks in the group. 96 | */ 97 | public init(tallyMarks: [RomanNumeralTallyMark]) { 98 | self.tallyMarks = tallyMarks 99 | } 100 | } 101 | 102 | // MARK: - Operators Extension 103 | 104 | extension RomanNumeralTallyMarkGroup { 105 | // MARK: Public Static Interface 106 | 107 | public static func + ( 108 | left: RomanNumeralTallyMarkGroup, 109 | right: RomanNumeralTallyMarkGroup 110 | ) -> RomanNumeralTallyMarkGroup { 111 | RomanNumeralTallyMarkGroup(tallyMarks: left.tallyMarks + right.tallyMarks) 112 | } 113 | } 114 | 115 | // MARK: - Comparable Extension 116 | 117 | extension RomanNumeralTallyMarkGroup: Comparable { 118 | // MARK: Public Static Interface 119 | 120 | public static func < (lhs: RomanNumeralTallyMarkGroup, rhs: RomanNumeralTallyMarkGroup) -> Bool { 121 | lhs.tallyMarks.count < rhs.tallyMarks.count 122 | } 123 | } 124 | 125 | // MARK: - CustomDebugStringConvertible Extension 126 | 127 | extension RomanNumeralTallyMarkGroup: CustomDebugStringConvertible { 128 | public var debugDescription: String { 129 | tallyMarks.map { $0.debugDescription }.joined() 130 | } 131 | } 132 | 133 | // MARK: - CustomStringConvertible Extension 134 | 135 | extension RomanNumeralTallyMarkGroup: CustomStringConvertible { 136 | public var description: String { 137 | tallyMarks.map { $0.description }.joined() 138 | } 139 | } 140 | 141 | // MARK: - Sequence Extension 142 | 143 | extension RomanNumeralTallyMarkGroup: Sequence { 144 | // MARK: Public Typealiases 145 | 146 | public typealias Iterator = IndexingIterator<[RomanNumeralTallyMark]> 147 | 148 | // MARK: Public Instance Interface 149 | 150 | public func makeIterator() -> IndexingIterator<[RomanNumeralTallyMark]> { 151 | tallyMarks.makeIterator() 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RomanNumeralKit 2 | 3 | [![CocoaPods Version](https://img.shields.io/cocoapods/v/RomanNumeralKit.svg)]() 4 | [![Languages](https://img.shields.io/badge/languages-Swift-orange.svg)]() 5 | [![Platform](https://img.shields.io/cocoapods/p/RomanNumeralKit.svg)]() 6 | 7 | [![Build Status](https://travis-ci.org/kylehughes/RomanNumeralKit.svg?branch=mainline)](https://travis-ci.org/kylehughes/RomanNumeralKit) 8 | [![codecov](https://codecov.io/gh/kylehughes/RomanNumeralKit/branch/mainline/graph/badge.svg)](https://codecov.io/gh/kylehughes/RomanNumeralKit) 9 | 10 | First-class Roman numeral support for Swift. 11 | 12 | *When in Rome, code as the Romans code.* 13 | 14 | ## Introduction 15 | 16 | Meaningful usage of this framework requires understanding what Roman numerals are. Background information can be found [on Wikipedia](https://en.wikipedia.org/wiki/Roman_numerals). 17 | 18 | ### Features 19 | 20 | - [x] Constants provided for all 3,999 standard Roman numerals. 21 | - [x] Support for subtractive and additive notations. 22 | - [x] Arithmetic using Roman-numeral-oriented algorithms - no integer calculations! 23 | - [x] Conversions to-and-from popular types (e.g. `String`, `Int`). 24 | - [x] Extensions for real-world usage (e.g. copyright text). 25 | - [x] Conformance to all applicable numeric protocols. 26 | 27 | ### Limitations 28 | 29 | #### Fixed Numerical Range 30 | 31 | Standard Roman numerals as we understand them were limited to values from 1 to 3,999. There is no concept of 0. Modern scholars have proposed extensions of the numeric system to support values greater than 3,999 but we do not recognize any of these extensions and decry the proposers to be heretics. 32 | 33 | Most programs do not deal with numbers higher than 3,999, and the world won't exist past the year 3,999 in the Gregorian calendar, so there is no need to worry. 34 | 35 | #### iPhone Model Names 36 | 37 | RomanNumeralKit does not support conversions to-and-from recent iPhone model names such as "Xs". 38 | 39 | ## Requirements 40 | 41 | - iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ 42 | - Xcode 10.2+ 43 | - Swift 5+ 44 | 45 | ## Installation 46 | 47 | ### CocoaPods 48 | 49 | Add RomanNumeralKit to your `Podfile`: 50 | 51 | ```ruby 52 | pod 'RomanNumeralKit', '~> 1.0.0` 53 | ``` 54 | 55 | Please visit the [CocoaPods website](https://cocoapods.org/) for general CocoaPods usage and installation instructions. 56 | 57 | ### Swift Package Manager 58 | 59 | Add RomanNumeralKit to the `dependencies` value of your `Package.swift`: 60 | 61 | ```swift 62 | dependencies: [ 63 | .package(url: "https://github.com/kylehughes/RomanNumeralKit.git", from: "1.0.0") 64 | ] 65 | ``` 66 | 67 | ## Usage 68 | 69 | Import `RomanNumeralKit` at the top of the Swift file you want to use it in. 70 | 71 | ```swift 72 | import RomanNumeralKit 73 | ``` 74 | 75 | ### Constants 76 | 77 | Constants are provided for all valid Roman numerals, from 1 to 3,999. You should never need to use an initializer 78 | unless you are doing conversions from other types. 79 | 80 | All constants can be accessed directly by their using their uppercase Unicode characters. 81 | 82 | ```swift 83 | print(MMCDIX) // Prints "MMCDIX" 84 | print(MMCDIX.symbols) // Prints "[M, M, C, D, I, X]" 85 | 86 | XCTAssertEqual(MMCDIX, RomanNumeral(.M, .M, .C, .D, .I, .X)) // True 87 | ``` 88 | 89 | ### Conversions 90 | 91 | We provide convenient mechanisms to convert `RomanNumeral`s to-and-from popular types. 92 | 93 | It should be noted that these are true conversions: the backing values of `RomanNumeral` instances are groups of tally 94 | marks. We do not hold `Int`references because it would not be in the spirit of the framework. 95 | 96 | #### Constructors 97 | 98 | Constructors are provided to convert `Int`s and `String`s to `RomanNumeral`s. 99 | 100 | ```swift 101 | print(RomanNumeral(from: 2409)) // Prints "MMCDIX" 102 | print(RomanNumeral(from: "MMCDIX")) // Prints "MMCDIX" 103 | ``` 104 | 105 | We also support conversions from `Int` and `String` literals when the `RomanNumeral` type can be inferred. 106 | 107 | ```swift 108 | let numeralFromInt: RomanNumeral = 2409 109 | let numeralFromString: RomanNumeral = "MMCDIX" 110 | 111 | print(numeralFromInt) // Prints "MMCDIX" 112 | print(numeralFromString) // Prints "MMCDIX" 113 | ``` 114 | 115 | #### Properties 116 | 117 | Instance-level properties are provided to convert `RomanNumeral`s into `Int` and `String` values. 118 | 119 | ```swift 120 | print(MMCDIX.intValue) // Prints "2409" 121 | print(MMCDIX.stringValue) // Prints "MMCDIX" 122 | ``` 123 | 124 | We also provide various `*Convertible` protocols to allow types to return different `RomanNumeral` and 125 | `RomanNumeralSymbol` representations of themselves. 126 | 127 | ### Arithmetic 128 | 129 | Addition, subtraction, and multiplication operations are supported (and required) thanks to our conformance to the 130 | `Numeric` protocol. We use algorithms that allow us to directly manipulate the Roman numeral symbols as opposed to 131 | doing conversions to-and-from `Int`s. 132 | 133 | ```swift 134 | XCTAssertEqual(MD + CMIX, MMCDIX) // True 135 | XCTAssertEqual(MMM - DXCI, MMCDIX) // True 136 | XCTAssertEqual(XI * CCXIX, MMCDIX) // True 137 | ``` 138 | 139 | #### Performance 140 | 141 | Our committment to authenticity does have implications. 142 | 143 | The following table compares the performance `Int` arithmetic operations to Roman numeral arithmetic operations on a 144 | new MacBook Pro. 145 | 146 | | Operation (100x) | `Int` | `RomanNumeral` | % Slower | 147 | | ---------------- | ----------: | -------------: | -------------: | 148 | | Addition | 0.00000127s | 0.151s | 11,889,663.78% | 149 | | Subtraction | 0.00000151s | 0.0761s | 5,992,025.98% | 150 | | Multiplication | 0.00000204s | 0.0575s | 4,527,459.06% | 151 | 152 | It should be noted that this is much faster than any person from Ancient Rome could do arithmetic. Who can take issue 153 | with progress? 154 | 155 | ### Copyright Text 156 | 157 | The most useful feature we provide is automatic formatting of Copyright text. 158 | 159 | ```swift 160 | print(MDCCLXXVI.copyrightText) // Prints "Copyright © MDCCLXXVI" 161 | ``` 162 | 163 | ### Additive Notation 164 | 165 | The default notation for this framework is subtractive notation - that is what instances of `RomanNumeral`s represent. 166 | We provide the `AdditiveRomanNumeral` struct for initialization of numerals using additive notation. We also support 167 | conversions between the notations. 168 | 169 | Both notations implement the `RomanNumeralProtocol` protocol and support the same general interface. 170 | 171 | ```swift 172 | let additiveNumeral = AdditiveNotation(.M, .M, .C, .C, .C, .C, .V, .I, .I, .I, .I) 173 | 174 | print(additiveNumeral) // Prints "MMCCCCVIIII" 175 | print(additiveNumeral.intValue) // Prints "2409" 176 | 177 | XCTAssertEqual(additiveRomanNumeral.romanNumeral, MMCDIX) // True 178 | XCTAssertEqual(MMCDIX.additiveRomanNumeral, additiveRomanNumeral) // True 179 | ``` 180 | 181 | ### Extensions 182 | 183 | We provide a variety of extensions on existing Swift types to make common operations easier. 184 | 185 | #### `Calendar` & `DateComponent` Extensions 186 | 187 | `Calendar` objects, and the `DateComponents` they produce, are able to convert years into `RomanNumeral`s. 188 | 189 | ```swift 190 | if let currentYear = Calendar.current.currentYearAsRomanNumeral { 191 | print(currentYear) // Prints "MMXIX" 192 | print(currentYear.intValue) // Prints "2019" 193 | print(currentYear.copyrightText) // Prints "Copyright © MMXIX" 194 | } 195 | 196 | if let americasBirthYear = Calendar.current.yearAsRomanNumeral(fromDate: americasBirthDate) { 197 | print(americasBirthYear) // Prints "MDCCLXXVI" 198 | print(americasBirthYear.intValue) // Prints "1776" 199 | print(americasBirthYear.copyrightText) // Prints "Copyright © MDCCLXXVI" 200 | } 201 | ``` 202 | 203 | #### `Int` & `String` Extensions 204 | 205 | We conform `Int` and `String` to the `*RomanNumeralConvertible` protocols to complete the ouroboros with these 206 | foundational types. 207 | 208 | ```swift 209 | print(2409.romanNumeral) // Prints "MMCDIX" 210 | print(2409.additiveRomanNumeral) // Prints "MMCCCCVIIII" 211 | print("MMCDIX".romanNumeral) // Prints "MMCDIX" 212 | print("MMCCCCVIIII".additiveRomanNumeral) // Prints "MMCCCCVIIII" 213 | ``` 214 | 215 | ## Authors 216 | 217 | Kyle Hughes 218 | 219 | [![my twitter][social_twitter_image]][social_twitter_url] 220 | 221 | [social_twitter_image]: https://img.shields.io/badge/Twitter-@KyleHughes-blue.svg 222 | [social_twitter_url]: https://www.twitter.com/KyleHughes 223 | 224 | ## Contributions 225 | 226 | `RomanNumeralKit` is not accepting source contributions at this time. 227 | 228 | ## License 229 | 230 | `RomanNumeralKit` is available under the MIT License. 231 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Numeric System/RomanNumeralTallyMarkGroupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralTallyMarkGroupTests.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class RomanNumeralTallyMarkGroupTests: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() {} 34 | 35 | override func tearDown() {} 36 | 37 | // MARK: Tests 38 | 39 | func test_init_numberOfTallyMarks() { 40 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 0).tallyMarks.count, 0) 41 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1).tallyMarks.count, 1) 42 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 4).tallyMarks.count, 4) 43 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 5).tallyMarks.count, 5) 44 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 9).tallyMarks.count, 9) 45 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 10).tallyMarks.count, 10) 46 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 40).tallyMarks.count, 40) 47 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 50).tallyMarks.count, 50) 48 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 90).tallyMarks.count, 90) 49 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 100).tallyMarks.count, 100) 50 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 400).tallyMarks.count, 400) 51 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 500).tallyMarks.count, 500) 52 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 900).tallyMarks.count, 900) 53 | XCTAssertEqual(RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1000).tallyMarks.count, 1000) 54 | } 55 | 56 | func test_init_tallyMarks() { 57 | // Given... 58 | 59 | let I = RomanNumeralTallyMark() 60 | 61 | // Then... 62 | 63 | XCTAssertEqual(RomanNumeralTallyMarkGroup(tallyMarks: [I]).tallyMarks, [I]) 64 | XCTAssertEqual(RomanNumeralTallyMarkGroup(tallyMarks: [I, I, I, I, I]).tallyMarks, [I, I, I, I, I]) 65 | } 66 | } 67 | 68 | // MARK: - Comparable Tests 69 | 70 | extension RomanNumeralTallyMarkGroupTests { 71 | func test_lessThan() { 72 | XCTAssert(RomanNumeralTallyMarkGroup.one < RomanNumeralTallyMarkGroup.four) 73 | XCTAssert(RomanNumeralTallyMarkGroup.four < RomanNumeralTallyMarkGroup.five) 74 | XCTAssert(RomanNumeralTallyMarkGroup.five < RomanNumeralTallyMarkGroup.nine) 75 | XCTAssert(RomanNumeralTallyMarkGroup.nine < RomanNumeralTallyMarkGroup.ten) 76 | XCTAssert(RomanNumeralTallyMarkGroup.ten < RomanNumeralTallyMarkGroup.forty) 77 | XCTAssert(RomanNumeralTallyMarkGroup.forty < RomanNumeralTallyMarkGroup.fifty) 78 | XCTAssert(RomanNumeralTallyMarkGroup.fifty < RomanNumeralTallyMarkGroup.ninety) 79 | XCTAssert(RomanNumeralTallyMarkGroup.ninety < RomanNumeralTallyMarkGroup.oneHundred) 80 | XCTAssert(RomanNumeralTallyMarkGroup.oneHundred < RomanNumeralTallyMarkGroup.fourHundred) 81 | XCTAssert(RomanNumeralTallyMarkGroup.fourHundred < RomanNumeralTallyMarkGroup.fiveHundred) 82 | XCTAssert(RomanNumeralTallyMarkGroup.fiveHundred < RomanNumeralTallyMarkGroup.nineHundred) 83 | XCTAssert(RomanNumeralTallyMarkGroup.nineHundred < RomanNumeralTallyMarkGroup.oneThousand) 84 | 85 | XCTAssertFalse(RomanNumeralTallyMarkGroup.four < RomanNumeralTallyMarkGroup.one) 86 | XCTAssertFalse(RomanNumeralTallyMarkGroup.five < RomanNumeralTallyMarkGroup.four) 87 | XCTAssertFalse(RomanNumeralTallyMarkGroup.nine < RomanNumeralTallyMarkGroup.five) 88 | XCTAssertFalse(RomanNumeralTallyMarkGroup.ten < RomanNumeralTallyMarkGroup.nine) 89 | XCTAssertFalse(RomanNumeralTallyMarkGroup.forty < RomanNumeralTallyMarkGroup.ten) 90 | XCTAssertFalse(RomanNumeralTallyMarkGroup.fifty < RomanNumeralTallyMarkGroup.forty) 91 | XCTAssertFalse(RomanNumeralTallyMarkGroup.ninety < RomanNumeralTallyMarkGroup.fifty) 92 | XCTAssertFalse(RomanNumeralTallyMarkGroup.oneHundred < RomanNumeralTallyMarkGroup.ninety) 93 | XCTAssertFalse(RomanNumeralTallyMarkGroup.fourHundred < RomanNumeralTallyMarkGroup.oneHundred) 94 | XCTAssertFalse(RomanNumeralTallyMarkGroup.fiveHundred < RomanNumeralTallyMarkGroup.fourHundred) 95 | XCTAssertFalse(RomanNumeralTallyMarkGroup.nineHundred < RomanNumeralTallyMarkGroup.fiveHundred) 96 | XCTAssertFalse(RomanNumeralTallyMarkGroup.oneThousand < RomanNumeralTallyMarkGroup.nineHundred) 97 | 98 | XCTAssertFalse(RomanNumeralTallyMarkGroup.one < RomanNumeralTallyMarkGroup.one) 99 | XCTAssertFalse(RomanNumeralTallyMarkGroup.four < RomanNumeralTallyMarkGroup.four) 100 | XCTAssertFalse(RomanNumeralTallyMarkGroup.five < RomanNumeralTallyMarkGroup.five) 101 | XCTAssertFalse(RomanNumeralTallyMarkGroup.nine < RomanNumeralTallyMarkGroup.nine) 102 | XCTAssertFalse(RomanNumeralTallyMarkGroup.ten < RomanNumeralTallyMarkGroup.ten) 103 | XCTAssertFalse(RomanNumeralTallyMarkGroup.forty < RomanNumeralTallyMarkGroup.forty) 104 | XCTAssertFalse(RomanNumeralTallyMarkGroup.fifty < RomanNumeralTallyMarkGroup.fifty) 105 | XCTAssertFalse(RomanNumeralTallyMarkGroup.ninety < RomanNumeralTallyMarkGroup.ninety) 106 | XCTAssertFalse(RomanNumeralTallyMarkGroup.oneHundred < RomanNumeralTallyMarkGroup.oneHundred) 107 | XCTAssertFalse(RomanNumeralTallyMarkGroup.fourHundred < RomanNumeralTallyMarkGroup.fourHundred) 108 | XCTAssertFalse(RomanNumeralTallyMarkGroup.fiveHundred < RomanNumeralTallyMarkGroup.fiveHundred) 109 | XCTAssertFalse(RomanNumeralTallyMarkGroup.nineHundred < RomanNumeralTallyMarkGroup.nineHundred) 110 | XCTAssertFalse(RomanNumeralTallyMarkGroup.oneThousand < RomanNumeralTallyMarkGroup.oneThousand) 111 | } 112 | } 113 | 114 | // MARK: - CustomDebugStringConvertible Tests 115 | 116 | extension RomanNumeralTallyMarkGroupTests { 117 | func test_customDebugStringConvertible() { 118 | XCTAssertEqual( 119 | RomanNumeralTallyMarkGroup.oneHundred.debugDescription, 120 | RomanNumeralTallyMarkGroup.oneHundred.tallyMarks.map { $0.debugDescription }.joined() 121 | ) 122 | } 123 | } 124 | 125 | // MARK: - CustomStringConvertible Tests 126 | 127 | extension RomanNumeralTallyMarkGroupTests { 128 | func test_customStringConvertible() { 129 | XCTAssertEqual( 130 | RomanNumeralTallyMarkGroup.oneHundred.description, 131 | RomanNumeralTallyMarkGroup.oneHundred.tallyMarks.map { $0.description }.joined() 132 | ) 133 | } 134 | } 135 | 136 | // MARK: - Operators Tests 137 | 138 | extension RomanNumeralTallyMarkGroupTests { 139 | func test_addition() { 140 | XCTAssertEqual( 141 | RomanNumeralTallyMarkGroup.one + RomanNumeralTallyMarkGroup.one, 142 | RomanNumeralTallyMarkGroup(numberOfTallyMarks: 2) 143 | ) 144 | XCTAssertEqual( 145 | RomanNumeralTallyMarkGroup.fifty + RomanNumeralTallyMarkGroup.oneHundred, 146 | RomanNumeralTallyMarkGroup(numberOfTallyMarks: 150) 147 | ) 148 | XCTAssertEqual( 149 | RomanNumeralTallyMarkGroup.oneThousand + RomanNumeralTallyMarkGroup.nineHundred, 150 | RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1900) 151 | ) 152 | } 153 | } 154 | 155 | // MARK: - Sequence Tests 156 | 157 | extension RomanNumeralTallyMarkGroupTests { 158 | func test_makeIterator() { 159 | // Given... 160 | 161 | let tallyMarkGroup = RomanNumeralTallyMarkGroup.oneHundred 162 | 163 | // When... 164 | 165 | var iterator = tallyMarkGroup.makeIterator() 166 | 167 | var numberOfIterations = 0 168 | while let _ = iterator.next() { 169 | numberOfIterations += 1 170 | } 171 | 172 | // Then... 173 | 174 | XCTAssertEqual(numberOfIterations, tallyMarkGroup.tallyMarks.count) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Subtractive Notation/SubtractiveRomanNumeralSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubtractiveRomanNumeralSymbol.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | public enum SubtractiveRomanNumeralSymbol: CaseIterable { 27 | /// The Latin word meaning "none" which is used in lieu of the Western Arabic value "0". 28 | case nulla 29 | 30 | /// The symbol representing the Western Arabic value "1". 31 | case I 32 | 33 | /// The compound subtractive symbol representing the Western Arabic value "4". 34 | case IV 35 | 36 | /// The symbol representing the Western Arabic value "5". 37 | case V 38 | 39 | /// The compound subtractive symbol representing the Western Arabic value "90". 40 | case IX 41 | 42 | /// The symbol representing the Western Arabic value "10". 43 | case X 44 | 45 | /// The compound subtractive symbol representing the Western Arabic value "90". 46 | case XL 47 | 48 | /// The symbol representing the Western Arabic value "50". 49 | case L 50 | 51 | /// The compound subtractive symbol representing the Western Arabic value "90". 52 | case XC 53 | 54 | /// The symbol representing the Western Arabic value "100". 55 | case C 56 | 57 | /// The compound subtractive symbol representing the Western Arabic value "400". 58 | case CD 59 | 60 | /// The symbol representing the Western Arabic value "500". 61 | case D 62 | 63 | /// The compound subtractive symbol representing the Western Arabic value "900". 64 | case CM 65 | 66 | /// The symbol representing the Western Arabic value "1000". 67 | case M 68 | 69 | // MARK: Public Static Interface 70 | 71 | /// All of the Roman numeral symbols, represented in ascending order by value using additive notation. 72 | public static let allAdditiveRomanNumeralSymbolsAscending = allSymbolsAscending 73 | .map { $0.additiveRomanNumeralSymbols } 74 | 75 | /// All of the Roman numeral symbols, represented in descending order by value using additive notation. 76 | public static let allAdditiveRomanNumeralSymbolsDescending = allSymbolsDescending 77 | .map { $0.additiveRomanNumeralSymbols } 78 | 79 | /// All of the Roman numeral symbols, represented in ascending order by value using regular Roman numeral symbols. 80 | public static let allRomanNumeralSymbolsAscending = allSymbolsAscending.map { $0.romanNumeralSymbols } 81 | 82 | // MARK: Public Instance Interface 83 | 84 | /** 85 | The current subtractive symbol represented using `RomanNumeralSymbols`. 86 | 87 | These symbols are directly converted and not expressed in additive notation. 88 | */ 89 | public var romanNumeralSymbols: [RomanNumeralSymbol] { 90 | switch self { 91 | case .nulla: 92 | return [.nulla] 93 | case .I: 94 | return [.I] 95 | case .IV: 96 | return [.I, .V] 97 | case .V: 98 | return [.V] 99 | case .IX: 100 | return [.I, .X] 101 | case .X: 102 | return [.X] 103 | case .XL: 104 | return [.X, .L] 105 | case .L: 106 | return [.L] 107 | case .XC: 108 | return [.X, .C] 109 | case .C: 110 | return [.C] 111 | case .CD: 112 | return [.C, .D] 113 | case .D: 114 | return [.D] 115 | case .CM: 116 | return [.C, .M] 117 | case .M: 118 | return [.M] 119 | } 120 | } 121 | } 122 | 123 | // MARK: - AdditiveRomanNumeralSymbolsConvertible Extension 124 | 125 | extension SubtractiveRomanNumeralSymbol: AdditiveRomanNumeralSymbolsConvertible { 126 | // MARK: Public Instance Interface 127 | 128 | public var additiveRomanNumeralSymbols: [RomanNumeralSymbol] { 129 | switch self { 130 | case .nulla: 131 | return [.nulla] 132 | case .I: 133 | return [.I] 134 | case .IV: 135 | return [.I, .I, .I, .I] 136 | case .V: 137 | return [.V] 138 | case .IX: 139 | return [.V, .I, .I, .I, .I] 140 | case .X: 141 | return [.X] 142 | case .XL: 143 | return [.X, .X, .X, .X] 144 | case .L: 145 | return [.L] 146 | case .XC: 147 | return [.L, .X, .X, .X, .X] 148 | case .C: 149 | return [.C] 150 | case .CD: 151 | return [.C, .C, .C, .C] 152 | case .D: 153 | return [.D] 154 | case .CM: 155 | return [.D, .C, .C, .C, .C] 156 | case .M: 157 | return [.M] 158 | } 159 | } 160 | } 161 | 162 | // MARK: - RawRepresentable Extension 163 | 164 | extension SubtractiveRomanNumeralSymbol: RawRepresentable { 165 | // MARK: Public Typealiases 166 | 167 | public typealias RawValue = RomanNumeralTallyMarkGroup 168 | 169 | // MARK: Public Initialization 170 | 171 | public init?(rawValue: RomanNumeralTallyMarkGroup) { 172 | switch rawValue { 173 | case .nulla: 174 | self = .nulla 175 | case .one: 176 | self = .I 177 | case .four: 178 | self = .IV 179 | case .five: 180 | self = .V 181 | case .nine: 182 | self = .IX 183 | case .ten: 184 | self = .X 185 | case .forty: 186 | self = .XL 187 | case .fifty: 188 | self = .L 189 | case .ninety: 190 | self = .XC 191 | case .oneHundred: 192 | self = .C 193 | case .fourHundred: 194 | self = .CD 195 | case .fiveHundred: 196 | self = .D 197 | case .nineHundred: 198 | self = .CM 199 | case .oneThousand: 200 | self = .M 201 | default: 202 | return nil 203 | } 204 | } 205 | 206 | // MARK: Public Instance Interface 207 | 208 | public var rawValue: RomanNumeralTallyMarkGroup { 209 | switch self { 210 | case .nulla: 211 | return .nulla 212 | case .I: 213 | return .one 214 | case .IV: 215 | return .four 216 | case .V: 217 | return .five 218 | case .IX: 219 | return .nine 220 | case .X: 221 | return .ten 222 | case .XL: 223 | return .forty 224 | case .L: 225 | return .fifty 226 | case .XC: 227 | return .ninety 228 | case .C: 229 | return .oneHundred 230 | case .CD: 231 | return .fourHundred 232 | case .D: 233 | return .fiveHundred 234 | case .CM: 235 | return .nineHundred 236 | case .M: 237 | return .oneThousand 238 | } 239 | } 240 | } 241 | 242 | // MARK: - RomanNumeralSymbolProtocol Extension 243 | 244 | extension SubtractiveRomanNumeralSymbol: RomanNumeralSymbolProtocol { 245 | // MARK: Public Static Properties 246 | 247 | public static let allSymbolsAscending: [SubtractiveRomanNumeralSymbol] = [.I, .IV, .V, .IX, .X, .XL, .L, .XC, .C, .CD, .D, .CM, .M] 248 | public static let allSymbolsDescending: [SubtractiveRomanNumeralSymbol] = allSymbolsAscending.reversed() 249 | 250 | // MARK: Public Initialization 251 | 252 | public init(from stringValue: String) throws { 253 | let potentialSymbol = SubtractiveRomanNumeralSymbol.allCases 254 | .filter { $0.stringValue == stringValue } 255 | .first 256 | 257 | guard let symbol = potentialSymbol else { 258 | throw RomanNumeralSymbolError.unrecognizedString(string: stringValue) 259 | } 260 | 261 | self = symbol 262 | } 263 | 264 | // MARK: Public Instance Interface 265 | 266 | public var stringValue: String { 267 | switch self { 268 | case .nulla: 269 | return "N" 270 | case .I: 271 | return "I" 272 | case .IV: 273 | return "IV" 274 | case .V: 275 | return "V" 276 | case .IX: 277 | return "IX" 278 | case .X: 279 | return "X" 280 | case .XL: 281 | return "XL" 282 | case .L: 283 | return "L" 284 | case .XC: 285 | return "XC" 286 | case .C: 287 | return "C" 288 | case .CD: 289 | return "CD" 290 | case .D: 291 | return "D" 292 | case .CM: 293 | return "CM" 294 | case .M: 295 | return "M" 296 | } 297 | } 298 | 299 | public var lesserSymbol: SubtractiveRomanNumeralSymbol? { 300 | switch self { 301 | case .nulla: 302 | return nil 303 | case .I: 304 | return nil 305 | case .IV: 306 | return .I 307 | case .V: 308 | return .IV 309 | case .IX: 310 | return .V 311 | case .X: 312 | return .IX 313 | case .XL: 314 | return .X 315 | case .L: 316 | return .XL 317 | case .XC: 318 | return .L 319 | case .C: 320 | return .XC 321 | case .CD: 322 | return .C 323 | case .D: 324 | return .CD 325 | case .CM: 326 | return .D 327 | case .M: 328 | return .CM 329 | } 330 | } 331 | } 332 | 333 | // MARK: - SubtractiveRomanNumeralSymbolConvertible Extension 334 | 335 | extension SubtractiveRomanNumeralSymbol: SubtractiveRomanNumeralSymbolConvertible { 336 | // MARK: Public Instance Interface 337 | 338 | public var subtractiveRomanNumeralSymbol: SubtractiveRomanNumeralSymbol { 339 | self 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Numeric System/RomanNumeralSymbolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbolTests.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class RomanNumeralSymbolTests: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() {} 34 | 35 | override func tearDown() {} 36 | 37 | // MARK: Tests 38 | 39 | func test_multiplication() { 40 | XCTAssertEqual(RomanNumeralSymbol.nulla * .nulla, []) 41 | 42 | for lhs in RomanNumeralSymbol.allSymbolsAscending { 43 | XCTAssertEqual(lhs * .nulla, []) 44 | } 45 | 46 | for lhs in RomanNumeralSymbol.allSymbolsAscending { 47 | for rhs in RomanNumeralSymbol.allSymbolsAscending { 48 | let expectedProduct = lhs.rawValue.tallyMarks.count * rhs.rawValue.tallyMarks.count 49 | let actualProduct = (lhs * rhs).map { $0.rawValue.tallyMarks.count }.reduce(0, +) 50 | XCTAssertEqual(actualProduct, expectedProduct) 51 | } 52 | } 53 | } 54 | 55 | func test_expandedIntoLesserSymbol() { 56 | XCTAssertEqual(RomanNumeralSymbol.nulla.expandedIntoLesserSymbol, []) 57 | XCTAssertEqual(RomanNumeralSymbol.I.expandedIntoLesserSymbol, [.I]) 58 | XCTAssertEqual(RomanNumeralSymbol.V.expandedIntoLesserSymbol, [.I, .I, .I, .I, .I]) 59 | XCTAssertEqual(RomanNumeralSymbol.X.expandedIntoLesserSymbol, [.V, .V]) 60 | XCTAssertEqual(RomanNumeralSymbol.L.expandedIntoLesserSymbol, [.X, .X, .X, .X, .X]) 61 | XCTAssertEqual(RomanNumeralSymbol.C.expandedIntoLesserSymbol, [.L, .L]) 62 | XCTAssertEqual(RomanNumeralSymbol.D.expandedIntoLesserSymbol, [.C, .C, .C, .C, .C]) 63 | XCTAssertEqual(RomanNumeralSymbol.M.expandedIntoLesserSymbol, [.D, .D]) 64 | } 65 | } 66 | 67 | // MARK: - AdditiveRomanNumeralConvertible Tests 68 | 69 | extension RomanNumeralSymbolTests { 70 | func test_additiveRomanNumeral() { 71 | for symbol in RomanNumeralSymbol.allSymbolsAscending { 72 | XCTAssertEqual(symbol.additiveRomanNumeral, try AdditiveRomanNumeral(symbol: symbol)) 73 | } 74 | } 75 | } 76 | 77 | // MARK: - AdditiveRomanNumeralSymbolConvertible Tests 78 | 79 | extension RomanNumeralSymbolTests { 80 | func test_basicRomanNumeralSymbolConvertible() { 81 | for symbol in RomanNumeralSymbol.allSymbolsAscending { 82 | XCTAssertEqual(symbol.additiveRomanNumeralSymbol, symbol) 83 | } 84 | } 85 | } 86 | 87 | // MARK: - AdditiveRomanNumeralSymbolsConvertible Tests 88 | 89 | extension RomanNumeralSymbolTests { 90 | func test_basicRomanNumeralSymbolsConvertible() { 91 | for symbol in RomanNumeralSymbol.allSymbolsAscending { 92 | XCTAssertEqual(symbol.additiveRomanNumeralSymbols, [symbol]) 93 | } 94 | } 95 | } 96 | 97 | // MARK: - Comparable Tests 98 | 99 | extension RomanNumeralSymbolTests { 100 | func test_lessThan() { 101 | XCTAssert(RomanNumeralSymbol.I < RomanNumeralSymbol.V) 102 | XCTAssert(RomanNumeralSymbol.V < RomanNumeralSymbol.X) 103 | XCTAssert(RomanNumeralSymbol.X < RomanNumeralSymbol.L) 104 | XCTAssert(RomanNumeralSymbol.L < RomanNumeralSymbol.C) 105 | XCTAssert(RomanNumeralSymbol.C < RomanNumeralSymbol.D) 106 | XCTAssert(RomanNumeralSymbol.D < RomanNumeralSymbol.M) 107 | 108 | XCTAssertFalse(RomanNumeralSymbol.V < RomanNumeralSymbol.I) 109 | XCTAssertFalse(RomanNumeralSymbol.X < RomanNumeralSymbol.V) 110 | XCTAssertFalse(RomanNumeralSymbol.L < RomanNumeralSymbol.X) 111 | XCTAssertFalse(RomanNumeralSymbol.C < RomanNumeralSymbol.L) 112 | XCTAssertFalse(RomanNumeralSymbol.D < RomanNumeralSymbol.C) 113 | XCTAssertFalse(RomanNumeralSymbol.M < RomanNumeralSymbol.D) 114 | 115 | XCTAssertFalse(RomanNumeralSymbol.I < RomanNumeralSymbol.I) 116 | XCTAssertFalse(RomanNumeralSymbol.V < RomanNumeralSymbol.V) 117 | XCTAssertFalse(RomanNumeralSymbol.X < RomanNumeralSymbol.X) 118 | XCTAssertFalse(RomanNumeralSymbol.L < RomanNumeralSymbol.L) 119 | XCTAssertFalse(RomanNumeralSymbol.C < RomanNumeralSymbol.C) 120 | XCTAssertFalse(RomanNumeralSymbol.D < RomanNumeralSymbol.D) 121 | XCTAssertFalse(RomanNumeralSymbol.M < RomanNumeralSymbol.M) 122 | } 123 | } 124 | 125 | // MARK: - CustomDebugStringConvertible Tests 126 | 127 | extension RomanNumeralSymbolTests { 128 | func test_debugDescription() { 129 | for symbol in RomanNumeralSymbol.allCases { 130 | XCTAssertEqual(symbol.debugDescription, symbol.stringValue) 131 | } 132 | } 133 | } 134 | 135 | // MARK: - CustomStringConvertible Tests 136 | 137 | extension RomanNumeralSymbolTests { 138 | func test_description() { 139 | for symbol in RomanNumeralSymbol.allCases { 140 | XCTAssertEqual(symbol.description, symbol.stringValue) 141 | } 142 | } 143 | } 144 | 145 | // MARK: - RawRepresentable Tests 146 | 147 | extension RomanNumeralSymbolTests { 148 | func test_init_rawValue_success() { 149 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .nulla), .nulla) 150 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .one), .I) 151 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .five), .V) 152 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .ten), .X) 153 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .fifty), .L) 154 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .oneHundred), .C) 155 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .fiveHundred), .D) 156 | XCTAssertEqual(RomanNumeralSymbol(rawValue: .oneThousand), .M) 157 | } 158 | 159 | func test_init_rawValue_invalid() { 160 | // Given... 161 | 162 | let two = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 2) 163 | let eleven = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 11) 164 | let oneHundredOne = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 101) 165 | let oneThousandOne = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1001) 166 | 167 | // Then... 168 | 169 | XCTAssertNil(RomanNumeralSymbol(rawValue: two)) 170 | XCTAssertNil(RomanNumeralSymbol(rawValue: eleven)) 171 | XCTAssertNil(RomanNumeralSymbol(rawValue: oneHundredOne)) 172 | XCTAssertNil(RomanNumeralSymbol(rawValue: oneThousandOne)) 173 | } 174 | } 175 | 176 | // MARK: - RomanNumeralSymbolProtocol Tests 177 | 178 | extension RomanNumeralSymbolTests { 179 | func test_init_fromString_success() { 180 | XCTAssertEqual(try RomanNumeralSymbol(from: "N"), .nulla) 181 | XCTAssertEqual(try RomanNumeralSymbol(from: "I"), .I) 182 | XCTAssertEqual(try RomanNumeralSymbol(from: "V"), .V) 183 | XCTAssertEqual(try RomanNumeralSymbol(from: "X"), .X) 184 | XCTAssertEqual(try RomanNumeralSymbol(from: "L"), .L) 185 | XCTAssertEqual(try RomanNumeralSymbol(from: "C"), .C) 186 | XCTAssertEqual(try RomanNumeralSymbol(from: "D"), .D) 187 | XCTAssertEqual(try RomanNumeralSymbol(from: "M"), .M) 188 | } 189 | 190 | func test_init_fromString_invalidCharacter() { 191 | XCTAssertThrowsError(try RomanNumeralSymbol(from: "R")) { 192 | switch $0 as? RomanNumeralSymbolError { 193 | case .none: 194 | XCTFail() 195 | case let .some(symbolError): 196 | switch symbolError { 197 | case .unrecognizedString: 198 | break 199 | } 200 | } 201 | } 202 | } 203 | 204 | func test_allSymbolsAscending() { 205 | XCTAssertEqual(RomanNumeralSymbol.allSymbolsAscending, [.I, .V, .X, .L, .C, .D, .M]) 206 | } 207 | 208 | func test_allSymbolsDescending() { 209 | XCTAssertEqual(RomanNumeralSymbol.allSymbolsDescending, [.M, .D, .C, .L, .X, .V, .I]) 210 | } 211 | 212 | func test_stringValue() { 213 | XCTAssertEqual(RomanNumeralSymbol.nulla.stringValue, "N") 214 | XCTAssertEqual(RomanNumeralSymbol.I.stringValue, "I") 215 | XCTAssertEqual(RomanNumeralSymbol.V.stringValue, "V") 216 | XCTAssertEqual(RomanNumeralSymbol.X.stringValue, "X") 217 | XCTAssertEqual(RomanNumeralSymbol.L.stringValue, "L") 218 | XCTAssertEqual(RomanNumeralSymbol.C.stringValue, "C") 219 | XCTAssertEqual(RomanNumeralSymbol.D.stringValue, "D") 220 | XCTAssertEqual(RomanNumeralSymbol.M.stringValue, "M") 221 | } 222 | 223 | func test_lesserSymbol() { 224 | XCTAssertEqual(RomanNumeralSymbol.nulla.lesserSymbol, nil) 225 | XCTAssertEqual(RomanNumeralSymbol.I.lesserSymbol, nil) 226 | XCTAssertEqual(RomanNumeralSymbol.V.lesserSymbol, .I) 227 | XCTAssertEqual(RomanNumeralSymbol.X.lesserSymbol, .V) 228 | XCTAssertEqual(RomanNumeralSymbol.L.lesserSymbol, .X) 229 | XCTAssertEqual(RomanNumeralSymbol.C.lesserSymbol, .L) 230 | XCTAssertEqual(RomanNumeralSymbol.D.lesserSymbol, .C) 231 | XCTAssertEqual(RomanNumeralSymbol.M.lesserSymbol, .D) 232 | } 233 | } 234 | 235 | // MARK: - RomanNumeralSymbolConvertible Tests 236 | 237 | extension RomanNumeralSymbolTests { 238 | func test_romanNumeralSymbol() { 239 | for symbol in RomanNumeralSymbol.allSymbolsAscending { 240 | XCTAssertEqual(symbol.romanNumeralSymbol, symbol) 241 | } 242 | } 243 | } 244 | 245 | // MARK: - RomanNumeralSymbolsConvertible Tests 246 | 247 | extension RomanNumeralSymbolTests { 248 | func test_romanNumeralSymbols() { 249 | for symbol in RomanNumeralSymbol.allSymbolsAscending { 250 | XCTAssertEqual(symbol.romanNumeralSymbols, [symbol]) 251 | } 252 | } 253 | } 254 | 255 | // MARK: - SubtractiveRomanNumeralSymbolsConvertible Tests 256 | 257 | extension RomanNumeralSymbolTests { 258 | func test_subtractiveRomanNumeralSymbols() { 259 | for symbol in RomanNumeralSymbol.allSymbolsAscending { 260 | let expectedSubtractiveSymbols: [SubtractiveRomanNumeralSymbol] = RomanNumeral 261 | .subtractiveSymbols(from: symbol.rawValue.tallyMarks.count) 262 | XCTAssertEqual(symbol.subtractiveRomanNumeralSymbols, expectedSubtractiveSymbols) 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralProtocol.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | Roman numerals are a base-10 numeral system that originated in ancient Rome. They are traditionally composed of seven 28 | symbols with fixed integer values. 29 | 30 | Powers of ten - thousands, hundreds, tens, and units - are written separately from left to right. 31 | 32 | The rules for composing the symbols into Roman numerals, and the integer value represented by the Roman numerals, 33 | are a product of the notation used by the numeral. The most common notation used in modern times is subtractive 34 | notation (see `RomanNumeral`). A common alternative is additive notation (see `AdditiveRomanNumeral`). 35 | 36 | Conceptually, the Roman numeral is an abbreviation for the number of tally marks that are equal to the integer 37 | representation of the Roman numeral. 38 | 39 | See `RomanNumeralSymbolProtocol` and `RomanNumeralSymbol` for the definitions of the traditional symbols. 40 | 41 | - SeeAlso: https://en.wikipedia.org/wiki/Roman_numerals 42 | */ 43 | public protocol RomanNumeralProtocol: CustomDebugStringConvertible, 44 | CustomStringConvertible, 45 | ExpressibleByStringLiteral, 46 | Numeric, 47 | Strideable, 48 | RomanNumeralSymbolsConvertible { 49 | // MARK: Typealiases 50 | 51 | associatedtype Stride = Int 52 | 53 | // MARK: Initialization 54 | 55 | /** 56 | Creates a new Roman numeral represented by the value of the given symbols. 57 | 58 | The given symbols must be in the appropriate order for the type of notation. 59 | 60 | The given symbols will be condensed to the most succint representation using the fewest number of symbols. The 61 | symbols that are ultimately produced by the Roman numeral may not be the same symbols as the ones passed in, but 62 | the value will be equivalent. 63 | 64 | The value of the resulting Roman numeral must be within the `minimum` and `maximum` values for the type of 65 | notation. 66 | 67 | - Parameter symbols: The symbols to construct the Roman numeral from. 68 | */ 69 | init(symbols: [RomanNumeralSymbol]) throws 70 | 71 | // MARK: Static Interface 72 | 73 | /// The maximum value that can be expressed by the implementing notation. 74 | static var maximum: Self { get } 75 | 76 | /// The minimum value that can be expressed by the implementing notation. 77 | static var minimum: Self { get } 78 | 79 | /** 80 | Condense the given symbols to their most succint representation using the fewest number of symbols. 81 | 82 | For example *VV* will be condensed to *X*. 83 | 84 | - Parameter symbols: The given symbols to condense. 85 | - Returns: The condensed form of the given symbols. 86 | */ 87 | static func condense(symbols: [RomanNumeralSymbol]) -> [RomanNumeralSymbol] 88 | 89 | /** 90 | Convert the given symbols into their cumulative integer value for the implementing notation. 91 | 92 | The order of the symbols should follow the rules of the implementing notation. 93 | 94 | - Parameter symbols: The symbols that should be represented as a cumulative integer value. 95 | - Returns: The cumulative integer value of the given symbols. 96 | */ 97 | static func int(from symbols: [RomanNumeralSymbol]) -> Int 98 | 99 | /** 100 | Convert the given integer into the corresponding symbols for the implementing notation. 101 | 102 | The returned symbols may not comprise a valid Roman numeral if the given integer value is not within the bounds 103 | defined by the implementing notation. 104 | 105 | - Parameter intValue: The integer to convert into a collection of symbols. 106 | - Returns: The symbolic representation of the given integer. 107 | */ 108 | static func symbols(from intValue: Int) -> [RomanNumeralSymbol] 109 | 110 | // MARK: Instance Interface 111 | 112 | /// The symbols that compose the Roman numeral. 113 | var symbols: [RomanNumeralSymbol] { get } 114 | 115 | /// The group of tally marks that back the value of the Roman numeral. 116 | var tallyMarkGroup: RomanNumeralTallyMarkGroup { get } 117 | } 118 | 119 | // MARK: - Public Extension 120 | 121 | public extension RomanNumeralProtocol { 122 | // MARK: Public Initialization 123 | 124 | /** 125 | Creates a new Roman numeral represented by the value of the given symbols. 126 | 127 | The given symbols must be in the appropriate order for the type of notation. 128 | 129 | The given symbols will be condensed to the most succint representation using the fewest number of symbols. The 130 | symbols that are ultimately produced by the Roman numeral may not be the same symbols as the ones passed in, but 131 | the value will be equivalent. 132 | 133 | The value of the resulting Roman numeral must be within the `minimum` and `maximum` values for the type of 134 | notation. 135 | 136 | - Parameter symbols: The symbols to construct the Roman numeral from. 137 | */ 138 | init(_ symbols: RomanNumeralSymbol...) throws { 139 | try self.init(symbols: symbols) 140 | } 141 | 142 | /** 143 | Creates a new Roman numeral represented by the value of the given symbol. 144 | 145 | The value of the resulting Roman numeral must be within the `minimum` and `maximum` values for the type of 146 | notation. 147 | 148 | - Parameter symbol: The symbol to construct the Roman numeral from. 149 | */ 150 | init(symbol: RomanNumeralSymbol) throws { 151 | try self.init(symbols: [symbol]) 152 | } 153 | 154 | /** 155 | Creates a Roman numeral equivalent to the value of the given integer. 156 | 157 | The value of the resulting Roman numeral must be within the `minimum` and `maximum` values for the type of 158 | notation. 159 | 160 | - Parameter int: The integer value to construct an equivalent Roman numeral from. 161 | */ 162 | init(from int: Int) throws { 163 | let symbols = Self.symbols(from: int) 164 | 165 | try self.init(symbols: symbols) 166 | } 167 | 168 | /** 169 | Creates a Roman numeral represented by the symbols found in the given string. 170 | 171 | The given symbols must be in the appropriate order for the type of notation. 172 | 173 | The given symbols will be condensed to the most succint representation using the fewest number of symbols. The 174 | symbols that are ultimately produced by the Roman numeral may not be the same symbols as the ones passed in, but 175 | the value will be equivalent. 176 | 177 | The value of the resulting Roman numeral must be within the `minimum` and `maximum` values for the type of 178 | notation. 179 | 180 | The string must exclusively be a valid Roman numeral with no extraneous characters. 181 | 182 | - Parameter string: The string representation of the symbols to construct the Roman numeral from. 183 | */ 184 | init(from string: String) throws { 185 | let symbols = try Self.symbols(from: string) 186 | 187 | try self.init(symbols: symbols) 188 | } 189 | 190 | // MARK: Public Static Interface 191 | 192 | /** 193 | Convert the given symbols into their corresponding string representation. 194 | 195 | - Parameter symbols: The given symbol to concatenate into a single string. 196 | - Returns: The string representation of all of the given symbols. 197 | */ 198 | static func string(from symbols: [RomanNumeralSymbol]) -> String { 199 | symbols.reduce("") { $0.appending(String($1.stringValue)) } 200 | } 201 | 202 | /** 203 | Convert the given string into the corresponding collection of symbols. 204 | 205 | The string must only contain characters that are valid Roman numeral symbols. 206 | 207 | - Parameter string: The given string to convert into individual symbols. 208 | - Returns: The symbols that were represented by the given string. 209 | */ 210 | static func symbols(from string: String) throws -> [RomanNumeralSymbol] { 211 | try string 212 | .lazy 213 | .map { String($0) } 214 | .map { try RomanNumeralSymbol(from: $0) } 215 | } 216 | 217 | // MARK: Public Instance Interface 218 | 219 | /** 220 | The current Roman numeral formatted as a copyright declaration. 221 | 222 | For example: "Copyright © MMX" 223 | */ 224 | var copyrightText: String { 225 | "Copyright © \(stringValue)" 226 | } 227 | 228 | /// The current Roman numeral converted into its integer equivalent. 229 | var intValue: Int { 230 | Self.int(from: symbols) 231 | } 232 | 233 | /// The current Roman numeral represented as a string. 234 | var stringValue: String { 235 | Self.string(from: symbols) 236 | } 237 | } 238 | 239 | // MARK: - Comparable Extension 240 | 241 | extension RomanNumeralProtocol { 242 | // MARK: Public Static Interface 243 | 244 | public static func < (lhs: Self, rhs: Self) -> Bool { 245 | guard lhs != rhs else { 246 | return false 247 | } 248 | 249 | return lhs.tallyMarkGroup < rhs.tallyMarkGroup 250 | } 251 | } 252 | 253 | // MARK: - CustomDebugStringConvertible Extension 254 | 255 | extension RomanNumeralProtocol { 256 | // MARK: Public Instance Interface 257 | 258 | public var debugDescription: String { 259 | stringValue 260 | } 261 | } 262 | 263 | // MARK: - CustomStringConvertible Extension 264 | 265 | extension RomanNumeralProtocol { 266 | // MARK: Public Instance Interface 267 | 268 | public var description: String { 269 | stringValue 270 | } 271 | } 272 | 273 | // MARK: - ExpressibleByIntegerLiteral Extension 274 | 275 | extension RomanNumeralProtocol { 276 | // MARK: Public Initialization 277 | 278 | public init(integerLiteral: Int) { 279 | do { 280 | try self.init(from: integerLiteral) 281 | } catch { 282 | switch error { 283 | case RomanNumeralError.valueGreaterThanMaximum: 284 | self = Self.maximum 285 | default: 286 | self = Self.minimum 287 | } 288 | } 289 | } 290 | } 291 | 292 | // MARK: - ExpressibleByStringLiteral Extension 293 | 294 | extension RomanNumeralProtocol { 295 | // MARK: Public Initialization 296 | 297 | public init(stringLiteral: String) { 298 | do { 299 | try self.init(from: stringLiteral) 300 | } catch { 301 | switch error { 302 | case RomanNumeralError.valueGreaterThanMaximum: 303 | self = Self.maximum 304 | default: 305 | self = Self.minimum 306 | } 307 | } 308 | } 309 | } 310 | 311 | // MARK: - Strideable Extension 312 | 313 | extension RomanNumeralProtocol { 314 | // MARK: Public Instance Interface 315 | 316 | public func advanced(by n: Int) -> Self { 317 | Self(integerLiteral: intValue + n) 318 | } 319 | 320 | public func distance(to other: Self) -> Int { 321 | intValue - other.intValue 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/RomanNumeralSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeralSymbol.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | Roman numeral symbols are a collection of letters from the Latin alphabet that are used to represent numbers in the 28 | numeric system of ancient Rome. Each symbol is shorthand for a specific number of tally marks. 29 | 30 | Traditional Roman numerals employ seven symbols, each with a fixed integer value. The number 0 does not have its own 31 | Roman numeral symbol, but the word "nulla" (the Latin word meaning "none") is used in lieu of 0. 32 | 33 | The backing values of the Roman numeral symbols, as found in their `rawValue`, are `RomanNumeralTallyMarkGroups` which 34 | are used to collect all of the corresponding `RomanNumeralTallyMark`s. Any representations of the symbols as Arabic 35 | numerals (e.g. `Int`) are conversions made by counting these tally marks. 36 | */ 37 | public enum RomanNumeralSymbol: CaseIterable { 38 | /// The Latin word meaning "none" which is used in lieu of the Western Arabic value "0". 39 | case nulla 40 | 41 | /// The symbol representing the Western Arabic value "1". 42 | case I 43 | 44 | /// The symbol representing the Western Arabic value "5". 45 | case V 46 | 47 | /// The symbol representing the Western Arabic value "10". 48 | case X 49 | 50 | /// The symbol representing the Western Arabic value "50". 51 | case L 52 | 53 | /// The symbol representing the Western Arabic value "100". 54 | case C 55 | 56 | /// The symbol representing the Western Arabic value "500". 57 | case D 58 | 59 | /// The symbol representing the Western Arabic value "1000". 60 | case M 61 | 62 | // MARK: Public Static Interface 63 | 64 | public static func * ( 65 | lhs: RomanNumeralSymbol, 66 | rhs: RomanNumeralSymbol 67 | ) -> [RomanNumeralSymbol] { 68 | switch lhs { 69 | case .nulla: 70 | return [] 71 | case .I: 72 | switch rhs { 73 | case .nulla: 74 | return [] 75 | default: 76 | return [rhs] 77 | } 78 | case .V: 79 | switch rhs { 80 | case .nulla: 81 | return [] 82 | case .I: 83 | return [.V] 84 | case .V: 85 | return [.X, .X, .V] 86 | case .X: 87 | return [.L] 88 | case .L: 89 | return [.C, .C, .L] 90 | case .C: 91 | return [.D] 92 | case .D: 93 | return [.M, M, .D] 94 | case .M: 95 | return Array(repeating: .M, count: 5) 96 | } 97 | case .X: 98 | switch rhs { 99 | case .nulla: 100 | return [] 101 | case .I: 102 | return [.X] 103 | case .V: 104 | return [.L] 105 | case .X: 106 | return [.C] 107 | case .L: 108 | return [.D] 109 | case .C: 110 | return [.M] 111 | case .D: 112 | return Array(repeating: .M, count: 5) 113 | case .M: 114 | return Array(repeating: .M, count: 10) 115 | } 116 | case .L: 117 | switch rhs { 118 | case .nulla: 119 | return [] 120 | case .I: 121 | return [.L] 122 | case .V: 123 | return [.C, .C, .L] 124 | case .X: 125 | return [.D] 126 | case .L: 127 | return [.M, .M, .D] 128 | case .C: 129 | return Array(repeating: .M, count: 5) 130 | case .D: 131 | return Array(repeating: .M, count: 25) 132 | case .M: 133 | return Array(repeating: .M, count: 50) 134 | } 135 | case .C: 136 | switch rhs { 137 | case .nulla: 138 | return [] 139 | case .I: 140 | return [.C] 141 | case .V: 142 | return [.D] 143 | case .X: 144 | return [.M] 145 | case .L: 146 | return Array(repeating: .M, count: 5) 147 | case .C: 148 | return Array(repeating: .M, count: 10) 149 | case .D: 150 | return Array(repeating: .M, count: 50) 151 | case .M: 152 | return Array(repeating: .M, count: 100) 153 | } 154 | case .D: 155 | switch rhs { 156 | case .nulla: 157 | return [] 158 | case .I: 159 | return [.D] 160 | case .V: 161 | return [.M, .M, .D] 162 | case .X: 163 | return Array(repeating: .M, count: 5) 164 | case .L: 165 | return Array(repeating: .M, count: 25) 166 | case .C: 167 | return Array(repeating: .M, count: 50) 168 | case .D: 169 | return Array(repeating: .M, count: 250) 170 | case .M: 171 | return Array(repeating: .M, count: 500) 172 | } 173 | case .M: 174 | switch rhs { 175 | case .nulla: 176 | return [] 177 | case .I: 178 | return [.M] 179 | case .V: 180 | return Array(repeating: .M, count: 5) 181 | case .X: 182 | return Array(repeating: .M, count: 10) 183 | case .L: 184 | return Array(repeating: .M, count: 50) 185 | case .C: 186 | return Array(repeating: .M, count: 100) 187 | case .D: 188 | return Array(repeating: .M, count: 500) 189 | case .M: 190 | return Array(repeating: .M, count: 1000) 191 | } 192 | } 193 | } 194 | 195 | // MARK: Public Instance Interface 196 | 197 | /** 198 | The representation of the current Roman numeral symbol as a collection of its lesser symbol. 199 | 200 | For example, the expansion of `L` is equal to `XXXXX`. 201 | 202 | - Note: The expansion of `I` is its identity because it has no lesser symbol with a valid value. The expansion of 203 | `nulla` is an empty `Array` for the same reason. 204 | */ 205 | public var expandedIntoLesserSymbol: [RomanNumeralSymbol] { 206 | switch self { 207 | case .nulla: 208 | return [] 209 | case .I: 210 | return [.I] 211 | case .V: 212 | return [.I, .I, .I, .I, .I] 213 | case .X: 214 | return [.V, .V] 215 | case .L: 216 | return [.X, .X, .X, .X, .X] 217 | case .C: 218 | return [.L, .L] 219 | case .D: 220 | return [.C, .C, .C, .C, .C] 221 | case .M: 222 | return [.D, .D] 223 | } 224 | } 225 | } 226 | 227 | // MARK: - AdditiveRomanNumeralSymbolConvertible Extension 228 | 229 | extension RomanNumeralSymbol: AdditiveRomanNumeralSymbolConvertible { 230 | // MARK: Public Instance Interface 231 | 232 | public var additiveRomanNumeralSymbol: RomanNumeralSymbol { 233 | self 234 | } 235 | } 236 | 237 | // MARK: - RawRepresentable Extension 238 | 239 | extension RomanNumeralSymbol: RawRepresentable { 240 | // MARK: Public Typealiases 241 | 242 | public typealias RawValue = RomanNumeralTallyMarkGroup 243 | 244 | // MARK: Public Initialization 245 | 246 | public init?(rawValue: RomanNumeralTallyMarkGroup) { 247 | switch rawValue { 248 | case .nulla: 249 | self = .nulla 250 | case .one: 251 | self = .I 252 | case .five: 253 | self = .V 254 | case .ten: 255 | self = .X 256 | case .fifty: 257 | self = .L 258 | case .oneHundred: 259 | self = .C 260 | case .fiveHundred: 261 | self = .D 262 | case .oneThousand: 263 | self = .M 264 | default: 265 | return nil 266 | } 267 | } 268 | 269 | // MARK: Public Instance Interface 270 | 271 | public var rawValue: RomanNumeralTallyMarkGroup { 272 | switch self { 273 | case .nulla: 274 | return .nulla 275 | case .I: 276 | return .one 277 | case .V: 278 | return .five 279 | case .X: 280 | return .ten 281 | case .L: 282 | return .fifty 283 | case .C: 284 | return .oneHundred 285 | case .D: 286 | return .fiveHundred 287 | case .M: 288 | return .oneThousand 289 | } 290 | } 291 | } 292 | 293 | // MARK: - RomanNumeralSymbolConvertible Extension 294 | 295 | extension RomanNumeralSymbol: RomanNumeralSymbolConvertible { 296 | // MARK: Public Instance Interface 297 | 298 | public var romanNumeralSymbol: RomanNumeralSymbol { 299 | self 300 | } 301 | } 302 | 303 | // MARK: - RomanNumeralSymbolProtocol Extension 304 | 305 | extension RomanNumeralSymbol: RomanNumeralSymbolProtocol { 306 | // MARK: Public Static Properties 307 | 308 | public static let allSymbolsAscending: [RomanNumeralSymbol] = [.I, .V, .X, .L, .C, .D, .M] 309 | 310 | public static let allSymbolsDescending: [RomanNumeralSymbol] = allSymbolsAscending.reversed() 311 | 312 | // MARK: Public Initialization 313 | 314 | public init(from stringValue: String) throws { 315 | let potentialSymbol = RomanNumeralSymbol.allCases 316 | .filter { $0.stringValue == stringValue } 317 | .first 318 | 319 | guard let symbol = potentialSymbol else { 320 | throw RomanNumeralSymbolError.unrecognizedString(string: stringValue) 321 | } 322 | 323 | self = symbol 324 | } 325 | 326 | // MARK: Public Instance Interface 327 | 328 | public var stringValue: String { 329 | switch self { 330 | case .nulla: 331 | return "N" 332 | case .I: 333 | return "I" 334 | case .V: 335 | return "V" 336 | case .X: 337 | return "X" 338 | case .L: 339 | return "L" 340 | case .C: 341 | return "C" 342 | case .D: 343 | return "D" 344 | case .M: 345 | return "M" 346 | } 347 | } 348 | 349 | public var lesserSymbol: RomanNumeralSymbol? { 350 | switch self { 351 | case .nulla: 352 | return nil 353 | case .I: 354 | return nil 355 | case .V: 356 | return .I 357 | case .X: 358 | return .V 359 | case .L: 360 | return .X 361 | case .C: 362 | return .L 363 | case .D: 364 | return .C 365 | case .M: 366 | return .D 367 | } 368 | } 369 | } 370 | 371 | // MARK: - SubtractiveRomanNumeralSymbolConvertible Extension 372 | 373 | extension RomanNumeralSymbol: SubtractiveRomanNumeralSymbolConvertible { 374 | // MARK: Public Instance Interface 375 | 376 | public var subtractiveRomanNumeralSymbol: SubtractiveRomanNumeralSymbol { 377 | switch self { 378 | case .nulla: 379 | return .nulla 380 | case .I: 381 | return .I 382 | case .V: 383 | return .V 384 | case .X: 385 | return .X 386 | case .L: 387 | return .L 388 | case .C: 389 | return .C 390 | case .D: 391 | return .D 392 | case .M: 393 | return .M 394 | } 395 | } 396 | } 397 | 398 | // MARK: - SubtractiveRomanNumeralSymbolsConvertible Extension 399 | 400 | extension RomanNumeralSymbol: SubtractiveRomanNumeralSymbolsConvertible { 401 | // MARK: Public Instance Interface 402 | 403 | public var subtractiveRomanNumeralSymbols: [SubtractiveRomanNumeralSymbol] { 404 | [subtractiveRomanNumeralSymbol] 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /Tests/RomanNumeralKitTests/Numeric System/Subtractive Notation/SubtractiveRomanNumeralSymbolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubtractiveRomanNumeralSymbolTests.swift 3 | // RomanNumeralKitTests 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import XCTest 27 | 28 | @testable import RomanNumeralKit 29 | 30 | class SubtractiveRomanNumeralSymbolTests: XCTestCase { 31 | // MARK: XCTestCase Implementation 32 | 33 | override func setUp() {} 34 | 35 | override func tearDown() {} 36 | 37 | // MARK: Tests 38 | 39 | func test_romanNumeralSymbols() { 40 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.nulla.romanNumeralSymbols, [.nulla]) 41 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.I.romanNumeralSymbols, [.I]) 42 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.IV.romanNumeralSymbols, [.I, .V]) 43 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.V.romanNumeralSymbols, [.V]) 44 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.IX.romanNumeralSymbols, [.I, .X]) 45 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.X.romanNumeralSymbols, [.X]) 46 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.XL.romanNumeralSymbols, [.X, .L]) 47 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.L.romanNumeralSymbols, [.L]) 48 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.XC.romanNumeralSymbols, [.X, .C]) 49 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.C.romanNumeralSymbols, [.C]) 50 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.CD.romanNumeralSymbols, [.C, .D]) 51 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.D.romanNumeralSymbols, [.D]) 52 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.CM.romanNumeralSymbols, [.C, .M]) 53 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.M.romanNumeralSymbols, [.M]) 54 | } 55 | } 56 | 57 | // MARK: - AdditiveRomanNumeralSymbolsConvertible Tests 58 | 59 | extension SubtractiveRomanNumeralSymbolTests { 60 | func test_basicRomanNumeralSymbols() { 61 | for symbol in SubtractiveRomanNumeralSymbol.allSymbolsAscending { 62 | let expectedBasicRomanNumeralSymbols = AdditiveRomanNumeral.symbols(from: symbol.rawValue.tallyMarks.count) 63 | XCTAssertEqual(symbol.additiveRomanNumeralSymbols, expectedBasicRomanNumeralSymbols) 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Comparable Tests 69 | 70 | extension SubtractiveRomanNumeralSymbolTests { 71 | func test_lessThan() { 72 | XCTAssert(SubtractiveRomanNumeralSymbol.I < SubtractiveRomanNumeralSymbol.IV) 73 | XCTAssert(SubtractiveRomanNumeralSymbol.IV < SubtractiveRomanNumeralSymbol.V) 74 | XCTAssert(SubtractiveRomanNumeralSymbol.V < SubtractiveRomanNumeralSymbol.IX) 75 | XCTAssert(SubtractiveRomanNumeralSymbol.IX < SubtractiveRomanNumeralSymbol.X) 76 | XCTAssert(SubtractiveRomanNumeralSymbol.X < SubtractiveRomanNumeralSymbol.XL) 77 | XCTAssert(SubtractiveRomanNumeralSymbol.XL < SubtractiveRomanNumeralSymbol.L) 78 | XCTAssert(SubtractiveRomanNumeralSymbol.L < SubtractiveRomanNumeralSymbol.XC) 79 | XCTAssert(SubtractiveRomanNumeralSymbol.XC < SubtractiveRomanNumeralSymbol.C) 80 | XCTAssert(SubtractiveRomanNumeralSymbol.C < SubtractiveRomanNumeralSymbol.CD) 81 | XCTAssert(SubtractiveRomanNumeralSymbol.CD < SubtractiveRomanNumeralSymbol.D) 82 | XCTAssert(SubtractiveRomanNumeralSymbol.D < SubtractiveRomanNumeralSymbol.CM) 83 | XCTAssert(SubtractiveRomanNumeralSymbol.CM < SubtractiveRomanNumeralSymbol.M) 84 | 85 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.IV < SubtractiveRomanNumeralSymbol.I) 86 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.V < SubtractiveRomanNumeralSymbol.IV) 87 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.IX < SubtractiveRomanNumeralSymbol.V) 88 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.X < SubtractiveRomanNumeralSymbol.IX) 89 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.XL < SubtractiveRomanNumeralSymbol.X) 90 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.L < SubtractiveRomanNumeralSymbol.XL) 91 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.XC < SubtractiveRomanNumeralSymbol.L) 92 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.C < SubtractiveRomanNumeralSymbol.XC) 93 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.CD < SubtractiveRomanNumeralSymbol.C) 94 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.D < SubtractiveRomanNumeralSymbol.CD) 95 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.CM < SubtractiveRomanNumeralSymbol.D) 96 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.M < SubtractiveRomanNumeralSymbol.CM) 97 | 98 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.I < SubtractiveRomanNumeralSymbol.I) 99 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.IV < SubtractiveRomanNumeralSymbol.IV) 100 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.V < SubtractiveRomanNumeralSymbol.V) 101 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.IX < SubtractiveRomanNumeralSymbol.IX) 102 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.X < SubtractiveRomanNumeralSymbol.X) 103 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.XL < SubtractiveRomanNumeralSymbol.XL) 104 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.L < SubtractiveRomanNumeralSymbol.L) 105 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.XC < SubtractiveRomanNumeralSymbol.XC) 106 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.C < SubtractiveRomanNumeralSymbol.C) 107 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.CD < SubtractiveRomanNumeralSymbol.CD) 108 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.D < SubtractiveRomanNumeralSymbol.D) 109 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.CM < SubtractiveRomanNumeralSymbol.CM) 110 | XCTAssertFalse(SubtractiveRomanNumeralSymbol.M < SubtractiveRomanNumeralSymbol.M) 111 | } 112 | } 113 | 114 | // MARK: - CustomDebugStringConvertible Tests 115 | 116 | extension SubtractiveRomanNumeralSymbolTests { 117 | func test_debugDescription() { 118 | for symbol in SubtractiveRomanNumeralSymbol.allCases { 119 | XCTAssertEqual(symbol.debugDescription, symbol.stringValue) 120 | } 121 | } 122 | } 123 | 124 | // MARK: - CustomStringConvertible Tests 125 | 126 | extension SubtractiveRomanNumeralSymbolTests { 127 | func test_description() { 128 | for symbol in SubtractiveRomanNumeralSymbol.allCases { 129 | XCTAssertEqual(symbol.description, symbol.stringValue) 130 | } 131 | } 132 | } 133 | 134 | // MARK: - RawRepresentable Tests 135 | 136 | extension SubtractiveRomanNumeralSymbolTests { 137 | func test_init_rawValue_success() { 138 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .nulla), .nulla) 139 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .one), .I) 140 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .four), .IV) 141 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .five), .V) 142 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .nine), .IX) 143 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .ten), .X) 144 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .forty), .XL) 145 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .fifty), .L) 146 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .ninety), .XC) 147 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .oneHundred), .C) 148 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .fourHundred), .CD) 149 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .fiveHundred), .D) 150 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .nineHundred), .CM) 151 | XCTAssertEqual(SubtractiveRomanNumeralSymbol(rawValue: .oneThousand), .M) 152 | } 153 | 154 | func test_init_rawValue_invalid() { 155 | // Given... 156 | 157 | let two = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 2) 158 | let eleven = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 11) 159 | let oneHundredOne = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 101) 160 | let oneThousandOne = RomanNumeralTallyMarkGroup(numberOfTallyMarks: 1001) 161 | 162 | // Then... 163 | 164 | XCTAssertNil(SubtractiveRomanNumeralSymbol(rawValue: two)) 165 | XCTAssertNil(SubtractiveRomanNumeralSymbol(rawValue: eleven)) 166 | XCTAssertNil(SubtractiveRomanNumeralSymbol(rawValue: oneHundredOne)) 167 | XCTAssertNil(SubtractiveRomanNumeralSymbol(rawValue: oneThousandOne)) 168 | } 169 | } 170 | 171 | // MARK: - RomanNumeralSymbolProtocol Tests 172 | 173 | extension SubtractiveRomanNumeralSymbolTests { 174 | func test_init_fromString_success() { 175 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "N"), .nulla) 176 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "I"), .I) 177 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "IV"), .IV) 178 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "V"), .V) 179 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "IX"), .IX) 180 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "X"), .X) 181 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "XL"), .XL) 182 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "L"), .L) 183 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "XC"), .XC) 184 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "C"), .C) 185 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "CD"), .CD) 186 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "D"), .D) 187 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "CM"), .CM) 188 | XCTAssertEqual(try SubtractiveRomanNumeralSymbol(from: "M"), .M) 189 | } 190 | 191 | func test_init_fromString_invalidCharacter() { 192 | XCTAssertThrowsError(try SubtractiveRomanNumeralSymbol(from: "R")) { 193 | switch $0 as? RomanNumeralSymbolError { 194 | case .none: 195 | XCTFail() 196 | case let .some(symbolError): 197 | switch symbolError { 198 | case .unrecognizedString: 199 | break 200 | } 201 | } 202 | } 203 | } 204 | 205 | func test_stringValue() { 206 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.nulla.stringValue, "N") 207 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.I.stringValue, "I") 208 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.IV.stringValue, "IV") 209 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.V.stringValue, "V") 210 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.IX.stringValue, "IX") 211 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.X.stringValue, "X") 212 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.XL.stringValue, "XL") 213 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.L.stringValue, "L") 214 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.XC.stringValue, "XC") 215 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.C.stringValue, "C") 216 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.CD.stringValue, "CD") 217 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.D.stringValue, "D") 218 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.CM.stringValue, "CM") 219 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.M.stringValue, "M") 220 | } 221 | 222 | func test_lesserSymbol() { 223 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.nulla.lesserSymbol, nil) 224 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.I.lesserSymbol, nil) 225 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.IV.lesserSymbol, .I) 226 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.V.lesserSymbol, .IV) 227 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.IX.lesserSymbol, .V) 228 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.X.lesserSymbol, .IX) 229 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.XL.lesserSymbol, .X) 230 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.L.lesserSymbol, .XL) 231 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.XC.lesserSymbol, .L) 232 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.C.lesserSymbol, .XC) 233 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.CD.lesserSymbol, .C) 234 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.D.lesserSymbol, .CD) 235 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.CM.lesserSymbol, .D) 236 | XCTAssertEqual(SubtractiveRomanNumeralSymbol.M.lesserSymbol, .CM) 237 | } 238 | } 239 | 240 | // MARK: - RomanNumeralConvertible Tests 241 | 242 | extension SubtractiveRomanNumeralSymbolTests { 243 | func test_subtractiveRomanNumeral() { 244 | for subtractiveSymbol in SubtractiveRomanNumeralSymbol.allSymbolsAscending { 245 | XCTAssertEqual( 246 | subtractiveSymbol.romanNumeral, 247 | try RomanNumeral(subtractiveSymbols: [subtractiveSymbol]) 248 | ) 249 | } 250 | } 251 | } 252 | 253 | // MARK: - SubtractiveRomanNumeralSymbolConvertible Tests 254 | 255 | extension SubtractiveRomanNumeralSymbolTests { 256 | func test_subtractiveRomanNumeralSymbolConvertible() { 257 | for subtractiveSymbol in SubtractiveRomanNumeralSymbol.allSymbolsAscending { 258 | XCTAssertEqual(subtractiveSymbol.subtractiveRomanNumeralSymbol, subtractiveSymbol) 259 | } 260 | } 261 | } 262 | 263 | // MARK: - SubtractiveRomanNumeralSymbolsConvertible Tests 264 | 265 | extension SubtractiveRomanNumeralSymbolTests { 266 | func test_subtractiveRomanNumeralSymbolsConvertible() { 267 | for subtractiveSymbol in SubtractiveRomanNumeralSymbol.allSymbolsAscending { 268 | XCTAssertEqual(subtractiveSymbol.subtractiveRomanNumeralSymbols, [subtractiveSymbol]) 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Additive Notation/AdditiveRomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditiveRomanNumeral.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A Roman numeral that represents its value using traditional symbols arranged in additive notation. The Roman numeral 28 | is condensed to its shortest representation using the fewest number of symbols. 29 | 30 | The value of an additive Roman numeral is accumalated by adding the symbols from left-to-right, where the symbols are 31 | arranged in order from greatest to least. 32 | 33 | For example, in additive notation *IIII* equals 4, and *VIIII* equals 9. There are no abbreviations like in 34 | subtractive notation. 35 | 36 | Conceptually, the Roman numeral is an abbreviation for the number of tally marks that are equal to the integer 37 | representation of the Roman numeral. 38 | 39 | See `RomanNumeralSymbolProtocol` and `RomanNumeralSymbol` for the definitions of the traditional symbols. 40 | 41 | - SeeAlso: https://en.wikipedia.org/wiki/Roman_numerals#Use_of_additive_notation 42 | */ 43 | public struct AdditiveRomanNumeral: AdditiveRomanNumeralSymbolsConvertible { 44 | // MARK: Public Static Properties 45 | 46 | public static let maximum = AdditiveRomanNumeral(unsafeSymbols: [.M, .M, .M, .D, .C, .C, .C, .C, .L, .X, .X, .X, .X, .V, .I, .I, .I, .I]) 47 | public static let minimum = AdditiveRomanNumeral(unsafeSymbols: [.I]) 48 | 49 | // MARK: Public Instance Properties 50 | 51 | public private(set) var additiveRomanNumeralSymbols: [RomanNumeralSymbol] 52 | 53 | // MARK: Internal Initialization 54 | 55 | internal init(unsafeSymbols: [RomanNumeralSymbol]) { 56 | additiveRomanNumeralSymbols = unsafeSymbols 57 | } 58 | 59 | // MARK: Internal Static Interface 60 | 61 | /** 62 | Convert the given additive symbols into their `SubtractiveRomanNumeralSymbol` equivalents. 63 | 64 | The symbols will be converted directly and will come out in additive notation using the alternate symbol type. 65 | 66 | - Parameter additiveSymbols: The additive symbols to convert. 67 | - Returns: The `SubtractiveRomanNumeralSymbol` equivalent of the additive symbols. 68 | */ 69 | internal static func convert( 70 | toSymbolEquivalentSubtractiveSymbols additiveSymbols: [RomanNumeralSymbol] 71 | ) -> [SubtractiveRomanNumeralSymbol] { 72 | var lastProcessedIndex = -1 73 | 74 | return additiveSymbols 75 | .lazy 76 | .enumerated() 77 | .compactMap { index, additiveSymbol in 78 | guard lastProcessedIndex < index else { 79 | return nil 80 | } 81 | 82 | let nextIndex = index + 1 83 | 84 | guard nextIndex < additiveSymbols.count else { 85 | lastProcessedIndex = index 86 | 87 | return additiveSymbol.subtractiveRomanNumeralSymbol 88 | } 89 | 90 | let nextAdditiveSymbol = additiveSymbols[nextIndex] 91 | 92 | guard 93 | let subtractiveSymbolIndex = SubtractiveRomanNumeralSymbol.allRomanNumeralSymbolsAscending 94 | .firstIndex(of: [additiveSymbol, nextAdditiveSymbol]) 95 | else { 96 | lastProcessedIndex = index 97 | 98 | return additiveSymbol.subtractiveRomanNumeralSymbol 99 | } 100 | 101 | lastProcessedIndex = nextIndex 102 | 103 | return SubtractiveRomanNumeralSymbol.allSymbolsAscending[subtractiveSymbolIndex] 104 | } 105 | } 106 | 107 | /** 108 | Convert the given additive symbols into their `SubtractiveRomanNumeralSymbol` equivalent using subtractive 109 | notation. 110 | 111 | The symbols will come out in subtractive notation with the same value as their given additive counterparts. 112 | 113 | - Parameter additiveSymbols: The additive symbols to convert. 114 | - Returns: The `SubtractiveRomanNumeralSymbol` equivalent of the additive symbols in subtractive notation. 115 | */ 116 | internal static func convert( 117 | toValueEquivalentSubtractiveSymbols additiveSymbols: [RomanNumeralSymbol] 118 | ) -> [SubtractiveRomanNumeralSymbol] { 119 | let enumeratedConvertedSubtractiveSymbols = SubtractiveRomanNumeralSymbol 120 | .allAdditiveRomanNumeralSymbolsDescending 121 | .enumerated() 122 | 123 | var remainingAdditiveSymbols = additiveSymbols.filter { $0 != .nulla } 124 | var subtractiveSymbols: [SubtractiveRomanNumeralSymbol] = [] 125 | 126 | while !remainingAdditiveSymbols.isEmpty { 127 | for (index, convertedSubtractiveSymbols) in enumeratedConvertedSubtractiveSymbols { 128 | guard remainingAdditiveSymbols.starts(with: convertedSubtractiveSymbols) else { 129 | continue 130 | } 131 | 132 | let subtractiveSymbol = SubtractiveRomanNumeralSymbol.allSymbolsDescending[index] 133 | subtractiveSymbols.append(subtractiveSymbol) 134 | remainingAdditiveSymbols.removeSubrange(0 ..< convertedSubtractiveSymbols.count) 135 | 136 | break 137 | } 138 | } 139 | 140 | return subtractiveSymbols 141 | } 142 | } 143 | 144 | // MARK: - AdditiveArithmetic Extension 145 | 146 | extension AdditiveRomanNumeral { 147 | // MARK: Public Static Interface 148 | 149 | public static func + ( 150 | lhs: AdditiveRomanNumeral, 151 | rhs: AdditiveRomanNumeral 152 | ) -> AdditiveRomanNumeral { 153 | let result: AdditiveRomanNumeral 154 | do { 155 | result = try add(lhs: lhs, rhs: rhs) 156 | } catch { 157 | switch error { 158 | case RomanNumeralError.valueGreaterThanMaximum: 159 | result = AdditiveRomanNumeral.maximum 160 | default: 161 | result = AdditiveRomanNumeral.minimum 162 | } 163 | } 164 | 165 | return result 166 | } 167 | 168 | public static func += (lhs: inout AdditiveRomanNumeral, rhs: AdditiveRomanNumeral) { 169 | lhs = lhs + rhs 170 | } 171 | 172 | public static func - ( 173 | lhs: AdditiveRomanNumeral, 174 | rhs: AdditiveRomanNumeral 175 | ) -> AdditiveRomanNumeral { 176 | let result: AdditiveRomanNumeral 177 | do { 178 | result = try subtract(lhs: lhs, rhs: rhs) 179 | } catch { 180 | switch error { 181 | case RomanNumeralError.valueGreaterThanMaximum: 182 | result = AdditiveRomanNumeral.maximum 183 | default: 184 | result = AdditiveRomanNumeral.minimum 185 | } 186 | } 187 | 188 | return result 189 | } 190 | 191 | public static func -= (lhs: inout AdditiveRomanNumeral, rhs: AdditiveRomanNumeral) { 192 | lhs = lhs - rhs 193 | } 194 | 195 | // MARK: Internal Static Interface 196 | 197 | /** 198 | - SeeAlso: http://turner.faculty.swau.edu/mathematics/materialslibrary/roman/ 199 | */ 200 | internal static func add( 201 | lhs: AdditiveRomanNumeral, 202 | rhs: AdditiveRomanNumeral 203 | ) throws -> AdditiveRomanNumeral { 204 | let concatenatedSymbols = lhs.additiveRomanNumeralSymbols + rhs.additiveRomanNumeralSymbols 205 | let condensedSymbols = AdditiveRomanNumeral.condense(symbols: concatenatedSymbols) 206 | 207 | return try AdditiveRomanNumeral(symbols: condensedSymbols) 208 | } 209 | 210 | /** 211 | - SeeAlso: http://turner.faculty.swau.edu/mathematics/materialslibrary/roman/ 212 | */ 213 | internal static func subtract( 214 | lhs: AdditiveRomanNumeral, 215 | rhs: AdditiveRomanNumeral 216 | ) throws -> AdditiveRomanNumeral { 217 | var eliminationResult = eliminateCommonSymbols( 218 | betweenLeft: lhs.additiveRomanNumeralSymbols, 219 | andRight: rhs.additiveRomanNumeralSymbols 220 | ) 221 | 222 | while !eliminationResult.right.isEmpty { 223 | let expandedRemainingLeftSymbols = try expandLargestSymbol( 224 | fromRight: eliminationResult.right, 225 | inLeft: eliminationResult.left 226 | ) 227 | eliminationResult = eliminateCommonSymbols( 228 | betweenLeft: expandedRemainingLeftSymbols, 229 | andRight: eliminationResult.right 230 | ) 231 | } 232 | 233 | return try AdditiveRomanNumeral(symbols: eliminationResult.left) 234 | } 235 | 236 | // MARK: Private Static Interface 237 | 238 | private static func eliminateCommonSymbols( 239 | betweenLeft left: [RomanNumeralSymbol], 240 | andRight right: [RomanNumeralSymbol] 241 | ) -> (left: [RomanNumeralSymbol], right: [RomanNumeralSymbol]) { 242 | var remainingLeftSymbols = left 243 | var remainingRightSymbols = right 244 | 245 | for rightSymbol in remainingRightSymbols.reversed() { 246 | guard 247 | let lastSymbolIndexInLeft = remainingLeftSymbols.lastIndex(of: rightSymbol), 248 | let rightSymbolIndex = remainingRightSymbols.lastIndex(of: rightSymbol) 249 | else { 250 | continue 251 | } 252 | 253 | remainingLeftSymbols.remove(at: lastSymbolIndexInLeft) 254 | remainingRightSymbols.remove(at: rightSymbolIndex) 255 | } 256 | 257 | return (left: remainingLeftSymbols, right: remainingRightSymbols) 258 | } 259 | 260 | private static func expandLargestSymbol( 261 | fromRight right: [RomanNumeralSymbol], 262 | inLeft left: [RomanNumeralSymbol] 263 | ) throws -> [RomanNumeralSymbol] { 264 | guard let greatestRightSymbol = right.max() else { 265 | throw RomanNumeralArithmeticError.ambiguousSubtractionError 266 | } 267 | 268 | let leftEnumeration = Array(left.enumerated()) 269 | 270 | guard let greaterLeftSymbolEnumeration = leftEnumeration.last(where: { greatestRightSymbol < $1 }) else { 271 | throw RomanNumeralArithmeticError.ambiguousSubtractionError 272 | } 273 | 274 | var expandedGreaterLeftSymbols: [RomanNumeralSymbol] = greaterLeftSymbolEnumeration 275 | .element 276 | .expandedIntoLesserSymbol 277 | 278 | while 279 | let expandedGreaterLeftSymbol = expandedGreaterLeftSymbols.first, 280 | expandedGreaterLeftSymbol != greatestRightSymbol { 281 | expandedGreaterLeftSymbols = expandedGreaterLeftSymbols.flatMap { $0.expandedIntoLesserSymbol } 282 | } 283 | 284 | guard !expandedGreaterLeftSymbols.isEmpty else { 285 | throw RomanNumeralArithmeticError.subtractionWhereRightValueIsGreaterThanLeftValue 286 | } 287 | 288 | let greaterLeftSymbolRange = greaterLeftSymbolEnumeration.offset ... greaterLeftSymbolEnumeration.offset 289 | 290 | var newLeftSymbols = left 291 | newLeftSymbols.replaceSubrange(greaterLeftSymbolRange, with: expandedGreaterLeftSymbols) 292 | 293 | return newLeftSymbols 294 | } 295 | } 296 | 297 | // MARK: - AdditiveRomanNumeralConvertible Extension 298 | 299 | extension AdditiveRomanNumeral: AdditiveRomanNumeralConvertible { 300 | // MARK: Public Instance Interface 301 | 302 | public var additiveRomanNumeral: AdditiveRomanNumeral? { 303 | self 304 | } 305 | } 306 | 307 | // MARK: - Numeric Extension 308 | 309 | extension AdditiveRomanNumeral { 310 | // MARK: Public Typealiases 311 | 312 | public typealias Magnitude = UInt16 313 | 314 | // MARK: Public Initialization 315 | 316 | public init?(exactly source: T) where T: BinaryInteger { 317 | try? self.init(from: Int(source)) 318 | } 319 | 320 | // MARK: Public Static Interface 321 | 322 | public static func * (lhs: AdditiveRomanNumeral, rhs: AdditiveRomanNumeral) -> AdditiveRomanNumeral { 323 | let result: AdditiveRomanNumeral 324 | do { 325 | result = try multiply(lhs: lhs, rhs: rhs) 326 | } catch { 327 | switch error { 328 | case RomanNumeralError.valueGreaterThanMaximum: 329 | result = AdditiveRomanNumeral.maximum 330 | default: 331 | result = AdditiveRomanNumeral.minimum 332 | } 333 | } 334 | 335 | return result 336 | } 337 | 338 | public static func *= (lhs: inout AdditiveRomanNumeral, rhs: AdditiveRomanNumeral) { 339 | lhs = lhs * rhs 340 | } 341 | 342 | // MARK: Public Instance Interface 343 | 344 | public var magnitude: UInt16 { 345 | UInt16(tallyMarkGroup.tallyMarks.count) 346 | } 347 | 348 | // MARK: Internal Static Interface 349 | 350 | /** 351 | - SeeAlso: http://turner.faculty.swau.edu/mathematics/materialslibrary/roman/ 352 | */ 353 | internal static func multiply(lhs: AdditiveRomanNumeral, rhs: AdditiveRomanNumeral) throws -> AdditiveRomanNumeral { 354 | var unsortedSymbolsFromProduct: [RomanNumeralSymbol] = [] 355 | for lhsSymbol in lhs.symbols { 356 | for rhsSymbol in rhs.symbols { 357 | unsortedSymbolsFromProduct.append(contentsOf: lhsSymbol * rhsSymbol) 358 | } 359 | } 360 | 361 | let sortedSymbolsFromProduct = unsortedSymbolsFromProduct.sorted { $0 > $1 } 362 | 363 | return try AdditiveRomanNumeral(symbols: sortedSymbolsFromProduct) 364 | } 365 | } 366 | 367 | // MARK: - RomanNumeralProtocol Extension 368 | 369 | extension AdditiveRomanNumeral: RomanNumeralProtocol { 370 | // MARK: Public Initialization 371 | 372 | public init(symbols: [RomanNumeralSymbol]) throws { 373 | guard symbols.isSorted(by: >=) else { 374 | throw RomanNumeralError.symbolsOutOfOrder 375 | } 376 | 377 | let condensedSymbols = AdditiveRomanNumeral.condense(symbols: symbols) 378 | let potentialRomanNumeral = AdditiveRomanNumeral(unsafeSymbols: condensedSymbols) 379 | 380 | guard AdditiveRomanNumeral.minimum <= potentialRomanNumeral else { 381 | throw RomanNumeralError.valueLessThanMinimum 382 | } 383 | 384 | guard potentialRomanNumeral <= AdditiveRomanNumeral.maximum else { 385 | throw RomanNumeralError.valueGreaterThanMaximum 386 | } 387 | 388 | self = potentialRomanNumeral 389 | } 390 | 391 | // MARK: Public Static Interface 392 | 393 | public static func condense(symbols: [RomanNumeralSymbol]) -> [RomanNumeralSymbol] { 394 | var condenser = AdditiveRomanNumeralSymbolCondenser() 395 | condenser.combine(symbols: symbols) 396 | 397 | return condenser.finalize() 398 | } 399 | 400 | public static func int(from symbols: [RomanNumeralSymbol]) -> Int { 401 | symbols.reduce(0) { $0 + $1.rawValue.tallyMarks.count } 402 | } 403 | 404 | public static func symbols(from intValue: Int) -> [RomanNumeralSymbol] { 405 | var remainingIntValue = intValue 406 | var convertedSymbols: [RomanNumeralSymbol] = [] 407 | 408 | RomanNumeralSymbol.allSymbolsDescending.forEach { symbol in 409 | let symbolCount: Int = remainingIntValue / symbol.rawValue.tallyMarks.count 410 | guard symbolCount > 0 else { 411 | return 412 | } 413 | 414 | let consecutiveSymbols = Array(repeating: symbol, count: symbolCount) 415 | convertedSymbols.append(contentsOf: consecutiveSymbols) 416 | remainingIntValue -= symbolCount * symbol.rawValue.tallyMarks.count 417 | } 418 | 419 | return convertedSymbols 420 | } 421 | 422 | // MARK: Public Instance Interface 423 | 424 | public var symbols: [RomanNumeralSymbol] { 425 | additiveRomanNumeralSymbols 426 | } 427 | 428 | public var tallyMarkGroup: RomanNumeralTallyMarkGroup { 429 | symbols.reduce(.nulla) { $0 + $1.rawValue } 430 | } 431 | } 432 | 433 | // MARK: - RomanNumeralSymbolsConvertible Extension 434 | 435 | extension AdditiveRomanNumeral { 436 | // MARK: Public Instance Interface 437 | 438 | public var romanNumeralSymbols: [RomanNumeralSymbol] { 439 | let subtractiveSymbols = AdditiveRomanNumeral.convert( 440 | toValueEquivalentSubtractiveSymbols: additiveRomanNumeralSymbols 441 | ) 442 | 443 | return RomanNumeral.convert(toSymbolEquivalentSymbols: subtractiveSymbols) 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /Sources/RomanNumeralKit/Numeric System/Subtractive Notation/RomanNumeral.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RomanNumeral.swift 3 | // RomanNumeralKit 4 | // 5 | // Copyright © 2019 Kyle Hughes. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | /** 27 | A Roman numeral that represents its value using traditional symbols arranged in subtractive notation. The Roman numeral 28 | is condensed to its shortest representation using the fewest number of symbols. 29 | 30 | The value of a Roman numeral is generally accumalated by adding the symbols from left-to-right, where the symbols are 31 | arranged in order from greatest to least. Subtractive notation introduces an abbreviation for some groups of symbols 32 | in order to shorten the length of the overall Roman numeral. It is represented by a lesser symbol preceding a greater 33 | symbol in the writing of the Roman numeral and can typically be read as subtracting the lesser symbol from the greater 34 | symbol. 35 | 36 | For example, in subtractive notation *IIII* (4) becomes *IV`* (1 less than 5). Similarly, in subtractive 37 | notation *LXXXX* (90) becomes *XC* (10 less than 100). 38 | 39 | Only specific combinations of symbols are recognized as being valid subtractive syntax. They are as follows: 40 | 41 | - *IV* = 4 42 | - *IX* = 9 43 | - *XL* = 40 44 | - *XC* = 90 45 | - *CD* = 400 46 | - *CM* = 900 47 | 48 | The subtractive grouping of two symbols, when viewed as a singular value, still follows the rule that symbols are 49 | ordered from greatest to least. 50 | 51 | For example, *XC* must succeed all symbols (subtractive or singular) whose value is greater than 90 and must precede 52 | all symbols (subtractive or singular) whose value is less than 90. So *MDCXCIV* (1,694) is a valid Roman numeral but 53 | *MDXCCIV* is not. 54 | 55 | Conceptually, the Roman numeral is an abbreviation for the number of tally marks that are equal to the integer 56 | representation of the Roman numeral. 57 | 58 | See `RomanNumeralSymbolProtocol` and `RomanNumeralSymbol` for the definitions of the traditional symbols. Internally, 59 | `RomanNumeral` represents its value using `SubtractiveRomanNumeralSymbol`s to explicitly define the subtractive cases 60 | and make value conversions easier. 61 | 62 | - SeeAlso: https://en.wikipedia.org/wiki/Roman_numerals#Description 63 | */ 64 | public struct RomanNumeral: SubtractiveRomanNumeralSymbolsConvertible { 65 | // MARK: Public Static Properties 66 | 67 | public static let maximum = RomanNumeral(unsafeSymbols: [.M, .M, .M, .CM, .XC, .IX]) 68 | public static let minimum = RomanNumeral(unsafeSymbols: [.I]) 69 | 70 | // MARK: Public Instance Properties 71 | 72 | public private(set) var subtractiveRomanNumeralSymbols: [SubtractiveRomanNumeralSymbol] 73 | 74 | // MARK: Public Initialization 75 | 76 | /** 77 | Creates a new Roman numeral represented by the value of the given subtractive symbols. 78 | 79 | The given symbols must be in the appropriate order for subtractive notation. 80 | 81 | The given symbols will be condensed to the most succint representation using the fewest number of symbols. The 82 | symbols that are ultimately produced by the Roman numeral may not be the same symbols as the ones passed in, but 83 | the value will be equivalent. 84 | 85 | The value of the resulting Roman numeral must be within the `minimum` and `maximum` values for subtractive 86 | notation. 87 | 88 | This differs from the protocol-defined `init(symbols:)` by using the special `SubtractiveRomanNumeralSymbol` type. 89 | Both are valid ways to construct subtractive Roman numerals. 90 | 91 | - Parameter subtractiveSymbols: The subtractive symbols to construct the Roman numeral from. 92 | */ 93 | public init(subtractiveSymbols: [SubtractiveRomanNumeralSymbol]) throws { 94 | guard subtractiveSymbols.isSorted(by: >=) else { 95 | throw RomanNumeralError.symbolsOutOfOrder 96 | } 97 | 98 | let condensedSubtractiveSymbols = RomanNumeral.condense(subtractiveSymbols: subtractiveSymbols) 99 | let potentialRomanNumeral = RomanNumeral(unsafeSymbols: condensedSubtractiveSymbols) 100 | 101 | guard RomanNumeral.minimum <= potentialRomanNumeral else { 102 | throw RomanNumeralError.valueLessThanMinimum 103 | } 104 | 105 | guard potentialRomanNumeral <= RomanNumeral.maximum else { 106 | throw RomanNumeralError.valueGreaterThanMaximum 107 | } 108 | 109 | self = potentialRomanNumeral 110 | } 111 | 112 | // MARK: Internal Initialization 113 | 114 | internal init(unsafeSymbols: [SubtractiveRomanNumeralSymbol]) { 115 | subtractiveRomanNumeralSymbols = unsafeSymbols 116 | } 117 | 118 | // MARK: Public Static Interface 119 | 120 | /** 121 | Condense the given subtractive symbols to their most succint representation using the fewest number of symbols. 122 | 123 | For example *VV* will be condensed to *X*. 124 | 125 | This differs from the protocol-defined `condense(symbols:)` by using the special `SubtractiveRomanNumeralSymbol` 126 | type. Both are valid ways to condense Roman numeral symbols. 127 | 128 | - Parameter subtractiveSymbols: The given subtractive symbols to condense. 129 | - Returns: The condensed form of the given subtractive symbols. 130 | */ 131 | public static func condense(subtractiveSymbols: [SubtractiveRomanNumeralSymbol]) -> [SubtractiveRomanNumeralSymbol] { 132 | let symbols = RomanNumeral.convert(toValueEquivalentSymbols: subtractiveSymbols) 133 | let condensedSymbols = AdditiveRomanNumeral.condense(symbols: symbols) 134 | 135 | return AdditiveRomanNumeral.convert(toValueEquivalentSubtractiveSymbols: condensedSymbols) 136 | } 137 | 138 | /** 139 | Convert the given subtractive symbols into their cumulative integer value. 140 | 141 | The order of the symbols should follow subtractive notation rules. 142 | 143 | This differs from the protocol-defined `int(from:)` by using the special `SubtractiveRomanNumeralSymbol` type. 144 | Both are valid ways to convert symbols into integers. 145 | 146 | - Parameter symbols: The subtractive symbols that should be represented as a cumulative integer value. 147 | - Returns: The cumulative integer value of the given subtractive symbols. 148 | */ 149 | public static func int(from subtractiveSymbols: [SubtractiveRomanNumeralSymbol]) -> Int { 150 | subtractiveSymbols.reduce(0) { $0 + $1.rawValue.tallyMarks.count } 151 | } 152 | 153 | /** 154 | Convert the given integer into the corresponding subtractive symbols. 155 | 156 | The returned symbols may not comprise a valid Roman numeral if the given integer value is not within the bounds 157 | defined by `minimum` and `maximum`. 158 | 159 | This differs from the protocol-defined `symbols(from:)` by using the special `SubtractiveRomanNumeralSymbol` type. 160 | Both are valid ways to convert integers into symbols. 161 | 162 | - Parameter intValue: The integer to convert into a collection of subtractive symbols. 163 | - Returns: The symbolic representation of the given integer. 164 | */ 165 | public static func subtractiveSymbols(from intValue: Int) -> [SubtractiveRomanNumeralSymbol] { 166 | var remainingIntValue = intValue 167 | var convertedSubtractiveSymbols: [SubtractiveRomanNumeralSymbol] = [] 168 | 169 | SubtractiveRomanNumeralSymbol.allSymbolsDescending.forEach { symbol in 170 | let symbolCount: Int = remainingIntValue / symbol.rawValue.tallyMarks.count 171 | 172 | guard symbolCount > 0 else { 173 | return 174 | } 175 | 176 | let consecutiveSymbols = Array(repeating: symbol, count: symbolCount) 177 | convertedSubtractiveSymbols.append(contentsOf: consecutiveSymbols) 178 | remainingIntValue -= symbolCount * symbol.rawValue.tallyMarks.count 179 | } 180 | 181 | return convertedSubtractiveSymbols 182 | } 183 | 184 | // MARK: Internal Static Interface 185 | 186 | /** 187 | Convert the given subtractive symbols into their `RomanNumeralSymbol` equivalents. 188 | 189 | The symbols will be converted directly and will come out in subtractive notation using the alternate symbol type. 190 | 191 | - Parameter subtractiveSymbols: The subtractive symbols to convert. 192 | - Returns: The `RomanNumeralSymbol` equivalent of the subtractive symbols. 193 | */ 194 | internal static func convert( 195 | toSymbolEquivalentSymbols subtractiveSymbols: [SubtractiveRomanNumeralSymbol] 196 | ) -> [RomanNumeralSymbol] { 197 | subtractiveSymbols.flatMap { $0.romanNumeralSymbols } 198 | } 199 | 200 | /** 201 | Convert the given subtractive symbols into their `RomanNumeralSymbol` equivalent using additive notation. 202 | 203 | The symbols will come out in additive notation with the same value as their given subtractive counterparts. 204 | 205 | - Parameter subtractiveSymbols: The subtractive symbols to convert. 206 | - Returns: The `RomanNumeralSymbol` equivalent of the subtractive symbols in additive notation. 207 | */ 208 | internal static func convert( 209 | toValueEquivalentSymbols subtractiveSymbols: [SubtractiveRomanNumeralSymbol] 210 | ) -> [RomanNumeralSymbol] { 211 | subtractiveSymbols.flatMap { $0.additiveRomanNumeralSymbols } 212 | } 213 | } 214 | 215 | // MARK: - AdditiveArithmetic Extension 216 | 217 | extension RomanNumeral { 218 | // MARK: Public Static Interface 219 | 220 | public static func + ( 221 | lhs: RomanNumeral, 222 | rhs: RomanNumeral 223 | ) -> RomanNumeral { 224 | let result: RomanNumeral 225 | do { 226 | result = try add(lhs: lhs, rhs: rhs) 227 | } catch { 228 | switch error { 229 | case RomanNumeralError.valueGreaterThanMaximum: 230 | result = RomanNumeral.maximum 231 | default: 232 | result = RomanNumeral.minimum 233 | } 234 | } 235 | 236 | return result 237 | } 238 | 239 | public static func += (lhs: inout RomanNumeral, rhs: RomanNumeral) { 240 | lhs = lhs + rhs 241 | } 242 | 243 | public static func - ( 244 | lhs: RomanNumeral, 245 | rhs: RomanNumeral 246 | ) -> RomanNumeral { 247 | let result: RomanNumeral 248 | do { 249 | result = try subtract(lhs: lhs, rhs: rhs) 250 | } catch { 251 | switch error { 252 | case RomanNumeralError.valueGreaterThanMaximum: 253 | result = RomanNumeral.maximum 254 | default: 255 | result = RomanNumeral.minimum 256 | } 257 | } 258 | 259 | return result 260 | } 261 | 262 | public static func -= (lhs: inout RomanNumeral, rhs: RomanNumeral) { 263 | lhs = lhs - rhs 264 | } 265 | 266 | // MARK: Internal Static Interface 267 | 268 | /** 269 | - SeeAlso: http://turner.faculty.swau.edu/mathematics/materialslibrary/roman/ 270 | */ 271 | internal static func add( 272 | lhs: RomanNumeral, 273 | rhs: RomanNumeral 274 | ) throws -> RomanNumeral { 275 | guard 276 | let lhsAdditiveRomanNumeral = lhs.additiveRomanNumeral, 277 | let rhsAdditiveRomanNumeral = rhs.additiveRomanNumeral 278 | else { 279 | throw RomanNumeralArithmeticError.ambiguousAdditionError 280 | } 281 | 282 | guard 283 | let result = try AdditiveRomanNumeral.add( 284 | lhs: lhsAdditiveRomanNumeral, 285 | rhs: rhsAdditiveRomanNumeral 286 | ) 287 | .romanNumeral 288 | else { 289 | throw RomanNumeralArithmeticError.ambiguousAdditionError 290 | } 291 | 292 | return result 293 | } 294 | 295 | /** 296 | - SeeAlso: http://turner.faculty.swau.edu/mathematics/materialslibrary/roman/ 297 | */ 298 | internal static func subtract( 299 | lhs: RomanNumeral, 300 | rhs: RomanNumeral 301 | ) throws -> RomanNumeral { 302 | guard 303 | let leftAdditiveRomanNumeral = lhs.additiveRomanNumeral, 304 | let rightAdditiveRomanNumeral = rhs.additiveRomanNumeral 305 | else { 306 | throw RomanNumeralArithmeticError.ambiguousSubtractionError 307 | } 308 | 309 | guard 310 | let result = try AdditiveRomanNumeral.subtract( 311 | lhs: leftAdditiveRomanNumeral, 312 | rhs: rightAdditiveRomanNumeral 313 | ) 314 | .romanNumeral 315 | else { 316 | throw RomanNumeralArithmeticError.ambiguousSubtractionError 317 | } 318 | 319 | return result 320 | } 321 | } 322 | 323 | // MARK: - AdditiveRomanNumeralSymbolsConvertible Extension 324 | 325 | extension RomanNumeral: AdditiveRomanNumeralSymbolsConvertible { 326 | // MARK: - Public Instance Interface 327 | 328 | public var additiveRomanNumeralSymbols: [RomanNumeralSymbol] { 329 | RomanNumeral.convert(toValueEquivalentSymbols: subtractiveRomanNumeralSymbols) 330 | } 331 | } 332 | 333 | // MARK: - Numeric Extension 334 | 335 | extension RomanNumeral { 336 | // MARK: Public Typealiases 337 | 338 | public typealias Magnitude = UInt16 339 | 340 | // MARK: Public Initialization 341 | 342 | public init?(exactly source: T) where T: BinaryInteger { 343 | try? self.init(from: Int(source)) 344 | } 345 | 346 | // MARK: Public Static Interface 347 | 348 | public static func * (lhs: RomanNumeral, rhs: RomanNumeral) -> RomanNumeral { 349 | let result: RomanNumeral 350 | do { 351 | result = try multiply(lhs: lhs, rhs: rhs) 352 | } catch { 353 | switch error { 354 | case RomanNumeralError.valueGreaterThanMaximum: 355 | result = RomanNumeral.maximum 356 | default: 357 | result = RomanNumeral.minimum 358 | } 359 | } 360 | 361 | return result 362 | } 363 | 364 | public static func *= (lhs: inout RomanNumeral, rhs: RomanNumeral) { 365 | lhs = lhs * rhs 366 | } 367 | 368 | // MARK: Public Instance Interface 369 | 370 | public var magnitude: UInt16 { 371 | UInt16(tallyMarkGroup.tallyMarks.count) 372 | } 373 | 374 | // MARK: Internal Static Interface 375 | 376 | /** 377 | - SeeAlso: http://turner.faculty.swau.edu/mathematics/materialslibrary/roman/ 378 | */ 379 | internal static func multiply( 380 | lhs: RomanNumeral, 381 | rhs: RomanNumeral 382 | ) throws -> RomanNumeral { 383 | guard 384 | let lhsAdditiveRomanNumeral = lhs.additiveRomanNumeral, 385 | let rhsAdditiveRomanNumeral = rhs.additiveRomanNumeral 386 | else { 387 | throw RomanNumeralArithmeticError.ambiguousSubtractionError 388 | } 389 | 390 | guard 391 | let result = try AdditiveRomanNumeral.multiply( 392 | lhs: lhsAdditiveRomanNumeral, 393 | rhs: rhsAdditiveRomanNumeral 394 | ) 395 | .romanNumeral 396 | else { 397 | throw RomanNumeralArithmeticError.ambiguousSubtractionError 398 | } 399 | 400 | return result 401 | } 402 | } 403 | 404 | // MARK: - RomanNumeralProtocol Extension 405 | 406 | extension RomanNumeral: RomanNumeralProtocol { 407 | // MARK: Public Initialization 408 | 409 | public init(symbols: [RomanNumeralSymbol]) throws { 410 | let convertedSubtractiveSymbols = AdditiveRomanNumeral.convert(toSymbolEquivalentSubtractiveSymbols: symbols) 411 | try self.init(subtractiveSymbols: convertedSubtractiveSymbols) 412 | } 413 | 414 | // MARK: Public Static Interface 415 | 416 | public static func condense(symbols: [RomanNumeralSymbol]) -> [RomanNumeralSymbol] { 417 | let convertedSubtractiveSymbols = AdditiveRomanNumeral.convert(toSymbolEquivalentSubtractiveSymbols: symbols) 418 | let condensedSubtractiveSymbols = condense(subtractiveSymbols: convertedSubtractiveSymbols) 419 | 420 | return RomanNumeral.convert(toSymbolEquivalentSymbols: condensedSubtractiveSymbols) 421 | } 422 | 423 | public static func int(from symbols: [RomanNumeralSymbol]) -> Int { 424 | let convertedSubtractiveSymbols = AdditiveRomanNumeral.convert(toSymbolEquivalentSubtractiveSymbols: symbols) 425 | 426 | return int(from: convertedSubtractiveSymbols) 427 | } 428 | 429 | public static func symbols(from intValue: Int) -> [RomanNumeralSymbol] { 430 | subtractiveSymbols(from: intValue).flatMap { $0.romanNumeralSymbols } 431 | } 432 | 433 | // MARK: Public Instance Interface 434 | 435 | public var symbols: [RomanNumeralSymbol] { 436 | RomanNumeral.convert(toSymbolEquivalentSymbols: subtractiveRomanNumeralSymbols) 437 | } 438 | 439 | public var tallyMarkGroup: RomanNumeralTallyMarkGroup { 440 | subtractiveRomanNumeralSymbols.reduce(.nulla) { $0 + $1.rawValue } 441 | } 442 | } 443 | 444 | // MARK: - RomanNumeralSymbolsConvertible Extension 445 | 446 | extension RomanNumeral { 447 | // MARK: Public Instance Interface 448 | 449 | public var romanNumeralSymbols: [RomanNumeralSymbol] { 450 | RomanNumeral.convert(toSymbolEquivalentSymbols: subtractiveRomanNumeralSymbols) 451 | } 452 | } 453 | 454 | // MARK: - RomanNumeralConvertible Extension 455 | 456 | extension RomanNumeral: RomanNumeralConvertible { 457 | // MARK: - Public Instance Interface 458 | 459 | public var romanNumeral: RomanNumeral? { 460 | self 461 | } 462 | } 463 | --------------------------------------------------------------------------------