├── .codeclimate.yml ├── .editorconfig ├── .github └── workflows │ ├── codecov.yml │ └── tests.yml ├── .gitignore ├── .mailmap ├── .spi.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.txt ├── ISO4217.json ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── Package@swift-6.swift ├── Plugins └── ISOStandardCodegenPlugin │ └── plugin.swift ├── README.md ├── Sources ├── Currency │ ├── CurrencyDescriptor.swift │ ├── CurrencyMint.swift │ ├── CurrencyValue+Algorithms.swift │ ├── CurrencyValue+Arithmetic.swift │ ├── CurrencyValue+StringRepresentation.swift │ ├── CurrencyValue.swift │ ├── Documentation.docc │ │ ├── Articles │ │ │ ├── currency_mathematics.md │ │ │ ├── custom_currencies.md │ │ │ ├── displaying_currencies.md │ │ │ └── minting_currencies.md │ │ ├── Currency.md │ │ └── Symbol Extensions │ │ │ ├── CurrencyMint.md │ │ │ └── CurrencyValue.md │ ├── Extensions │ │ ├── Decimal.swift │ │ └── Sequence+Currency.swift │ └── MinorUnitRepresentation.swift └── ISOStandardCodegen │ ├── Codegen+CurrencyDefinitions.swift │ ├── Codegen+DefinitionParsing.swift │ ├── Codegen+FileHeader.swift │ ├── Codegen+MintLookup.swift │ └── main.swift ├── Tests └── CurrencyTests │ ├── CurrencyDescriptorTests.swift │ ├── CurrencyMintTests.swift │ ├── CurrencyValue+AlgorithmsTests.swift │ ├── CurrencyValue+ArithmeticTests.swift │ ├── CurrencyValue+StringRepresentationTests.swift │ ├── CurrencyValueTests.swift │ └── Sequence+CurrencyTests.swift └── scripts └── generate_contributors_list.sh /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 6 6 | complex-logic: 7 | config: 8 | threshold: 5 9 | file-lines: 10 | config: 11 | threshold: 600 12 | method-complexity: 13 | config: 14 | threshold: 6 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 50 21 | nested-control-flow: 22 | config: 23 | threshold: 4 24 | return-statements: 25 | enabled: false 26 | similar-code: 27 | config: 28 | threshold: 40 29 | identical-code: 30 | config: 31 | threshold: 40 32 | exclude_patterns: 33 | - "docs/" 34 | - ".gitlab-ci/" 35 | - ".github/" 36 | - "scripts/" 37 | - "Tests/" 38 | - "*.md" 39 | - "*.resolved" 40 | - "*.txt" 41 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{h,m}] 12 | indent_size = 4 13 | 14 | [*.md] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | codecov: 17 | if: ${{ !(github.event.pull_request.draft || false) }} 18 | runs-on: macos-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Check Swift version 22 | run: swift --version 23 | - name: Run tests with code coverage 24 | run: swift test --enable-code-coverage 25 | - name: Upload code coverage 26 | uses: vapor/swift-codecov-action@v0.3 27 | with: 28 | codecov_token: ${{ secrets.CODECOV_TOKEN }} 29 | build_parameters: -c debug 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Package Tests 2 | 3 | on: pull_request 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | macos: 11 | if: ${{ !(github.event.pull_request.draft || false) }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | swift-config: 16 | - { xcode_version: "15.0.1", macos_version: 14 } 17 | - { xcode_version: "15.2", macos_version: 14 } 18 | - { xcode_version: "15.4", macos_version: 14 } 19 | - { xcode_version: "16.1", macos_version: 14 } 20 | - { xcode_version: "16.2", macos_version: 14 } 21 | - { xcode_version: "15.4", macos_version: 15 } 22 | - { xcode_version: "16.1", macos_version: 15 } 23 | - { xcode_version: "16.3", macos_version: 15 } 24 | runs-on: macos-${{ matrix.swift-config.macos_version }} 25 | name: Xcode ${{ matrix.swift-config.xcode_version }} - macOS ${{ matrix.swift-config.macos_version }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Check Swift version 29 | run: | 30 | sudo xcode-select -s /Applications/Xcode_${{ matrix.swift-config.xcode_version }}.app/ 31 | export TOOLCHAINS=swift 32 | swift --version 33 | - name: Run tests 34 | run: swift test 35 | 36 | linux: 37 | if: ${{ !(github.event.pull_request.draft || false) }} 38 | runs-on: ubuntu-latest 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | swift-config: 43 | - { version: "5.7", distro: amazonlinux2 } 44 | - { version: "5.7", distro: jammy } 45 | - { version: "5.7", distro: focal } 46 | - { version: "5.7", distro: bionic } 47 | - { version: "5.7", distro: centos7 } 48 | - { version: "5.8", distro: amazonlinux2 } 49 | - { version: "5.8", distro: rhel-ubi9 } 50 | - { version: "5.8", distro: focal } 51 | - { version: "5.8", distro: jammy } 52 | - { version: "5.8", distro: centos7 } 53 | - { version: "5.8", distro: bionic } 54 | - { version: "5.9", distro: amazonlinux2 } 55 | - { version: "5.9", distro: rhel-ubi9 } 56 | - { version: "5.9", distro: focal } 57 | - { version: "5.9", distro: jammy } 58 | - { version: "5.9", distro: centos7 } 59 | - { version: "5.10", distro: bookworm } 60 | - { version: "5.10", distro: amazonlinux2 } 61 | - { version: "5.10", distro: rhel-ubi9 } 62 | - { version: "5.10", distro: noble } 63 | - { version: "5.10", distro: fedora39 } 64 | - { version: "5.10", distro: focal } 65 | - { version: "5.10", distro: jammy } 66 | - { version: "5.10", distro: mantic } 67 | - { version: "5.10", distro: centos7 } 68 | - { version: "6.0.3", distro: bookworm } 69 | - { version: "6.0.3", distro: amazonlinux2 } 70 | - { version: "6.0.3", distro: rhel-ubi9 } 71 | - { version: "6.0.3", distro: focal } 72 | - { version: "6.0.3", distro: jammy } 73 | - { version: "6.1.0", distro: bookworm } 74 | - { version: "6.1.0", distro: amazonlinux2 } 75 | - { version: "6.1.0", distro: rhel-ubi9 } 76 | - { version: "6.1.0", distro: focal } 77 | - { version: "6.1.0", distro: jammy } 78 | container: swift:${{ matrix.swift-config.version }}-${{ matrix.swift-config.distro }} 79 | name: Swift ${{ matrix.swift-config.version }} - ${{ matrix.swift-config.distro }} 80 | steps: 81 | - uses: actions/checkout@v1 82 | - name: Run ${{ matrix.swift-config.version }} - ${{ matrix.swift-config.distro }} Tests 83 | run: swift test 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | *.xcworkspace/ 5 | xcuserdata/ 6 | .swiftpm 7 | 8 | Carthage/ 9 | *.resolved 10 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Nathan Harris 2 | Nathan Harris 3 | Nathan Harris <2nd.lt.harris@gmail.com> 4 | Tony Dam 5 | Tony Dam 6 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: ['Currency'] 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [SemVer](https://semver.org/) changes are documented for each release on the [releases page](https://github.com/peek-travel/swift-currency/releases). 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | To be a truly great community, SwiftCurrency needs to welcome developers from all walks of life, 3 | with different backgrounds, and with a wide range of experience. A diverse and friendly 4 | community will have more great ideas, more unique perspectives, and produce more great 5 | code. We will work diligently to make the SwiftCurrency community welcoming to everyone. 6 | 7 | To give clarity of what is expected of our members, SwiftCurrency has adopted the code of conduct 8 | defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source 9 | communities, and we think it articulates our values well. The full text is copied below: 10 | 11 | ### Contributor Code of Conduct v1.3 12 | As contributors and maintainers of this project, and in the interest of fostering an open and 13 | welcoming community, we pledge to respect all people who contribute through reporting 14 | issues, posting feature requests, updating documentation, submitting pull requests or patches, 15 | and other activities. 16 | 17 | We are committed to making participation in this project a harassment-free experience for 18 | everyone, regardless of level of experience, gender, gender identity and expression, sexual 19 | orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or 20 | nationality. 21 | 22 | Examples of unacceptable behavior by participants include: 23 | - The use of sexualized language or imagery 24 | - Personal attacks 25 | - Trolling or insulting/derogatory comments 26 | - Public or private harassment 27 | - Publishing other’s private information, such as physical or electronic addresses, without explicit permission 28 | - Other unethical or unprofessional conduct 29 | 30 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 31 | commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of 32 | Conduct, or to ban temporarily or permanently any contributor for other behaviors that they 33 | deem inappropriate, threatening, offensive, or harmful. 34 | 35 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 36 | consistently applying these principles to every aspect of managing this project. Project 37 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 38 | from the project team. 39 | 40 | This code of conduct applies both within project spaces and in public spaces when an 41 | individual is representing the project or its community. 42 | 43 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 44 | contacting a project maintainer by messaging them directly. All complaints will be reviewed and 45 | investigated and will result in a response that is deemed necessary and appropriate to the 46 | circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter 47 | of an incident. 48 | 49 | *This policy is adapted from the Contributor Code of Conduct [version 1.3.0](https://contributor-covenant.org/version/1/3/0/).* 50 | 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license your contribution to the community, and agree by submitting the patch 4 | that your contributions are licensed under the [MIT](https://opensource.org/licenses/MIT) (see [`LICENSE`](../LICENSE)). 5 | 6 | ## Submitting a Bug 7 | 8 | Please ensure to specify the following: 9 | 10 | * **SwiftCurrency** commit hash 11 | * Simplest possible steps to reproduce 12 | * A pull request with a failing test case is preferred, but it's just as fine to write it in the issue description 13 | * Environment Information 14 | * Locale of device(s) 15 | * OS version and output of `uname -a` 16 | * Swift version or output of `swift --version` 17 | 18 | ## Development 19 | 20 | ### Git Workflow 21 | 22 | `master` is always the development branch. 23 | 24 | For **minor** or **patch** SemVer changes, create a branch off of the tagged commit. 25 | 26 | ### Submitting a Pull Request 27 | 28 | A great PR that is likely to be merged quickly is: 29 | 30 | 1. Concise, with as few changes as needed to achieve the end result. 31 | 1. Tested, ensuring that regressions aren't introduced now or in the future. 32 | 1. Documented, adding API documentation as needed to cover new functions and properties. 33 | 1. Accompanied by a [great commit message](https://chris.beams.io/posts/git-commit/) 34 | 35 | ### Updating the ISO 4217 Currency List 36 | 37 | > You will need [**gyb**](https://github.com/NSHipster/swift-gyb) installed on your machine. 38 | 39 | After updating the [`Resources/ISO4217.csv`](/Resources/ISO4217.csv) with the latest from the [ISO Currency Workgroup](https://www.currency-iso.org/en/home/tables/table-a1.html), 40 | run the following command: 41 | 42 | ```bash 43 | find . -name '*.gyb' | \ 44 | while read file; do \ 45 | $(which gyb) --line-directive '' -o "${file%.gyb}" "$file"; \ 46 | done 47 | ``` 48 | 49 | # Contributor Conduct 50 | 51 | All contributors are expected to adhere to this project's [Code of Conduct](CODE_OF_CONDUCT.md). 52 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | For the purpose of tracking copyright, this is the list of individuals and 2 | organizations who have contributed source code to SwiftCurrency. 3 | 4 | For employees of an organization/company where the copyright of work done 5 | by employees of that company is held by the company itself, only the company 6 | needs to be listed here. 7 | 8 | ## COPYRIGHT HOLDERS 9 | 10 | - Peek Travel Inc. (all contributors with '@peek.com') 11 | 12 | ### Contributors 13 | 14 | - Nathan Harris 15 | - Santiago Carmona Gonzalez 16 | - Tony Dam 17 | 18 | **Updating this list** 19 | 20 | Please do not edit this file manually. It is generated using `./scripts/generate_contributors_list.sh`. If a name is misspelled or appearing multiple times: add an entry in `./.mailmap` 21 | -------------------------------------------------------------------------------- /ISO4217.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "CtryNm": "UNITED ARAB EMIRATES (THE)", 4 | "CcyNm": "UAE Dirham", 5 | "Ccy": "AED", 6 | "CcyNbr": 784, 7 | "CcyMnrUnts": 2, 8 | "CcyNm/_IsFund": "", 9 | "CcyNm/__text": "" 10 | }, 11 | { 12 | "CtryNm": "AFGHANISTAN", 13 | "CcyNm": "Afghani", 14 | "Ccy": "AFN", 15 | "CcyNbr": 971, 16 | "CcyMnrUnts": 2, 17 | "CcyNm/_IsFund": "", 18 | "CcyNm/__text": "" 19 | }, 20 | { 21 | "CtryNm": "ALBANIA", 22 | "CcyNm": "Lek", 23 | "Ccy": "ALL", 24 | "CcyNbr": 8, 25 | "CcyMnrUnts": 2, 26 | "CcyNm/_IsFund": "", 27 | "CcyNm/__text": "" 28 | }, 29 | { 30 | "CtryNm": "ARMENIA", 31 | "CcyNm": "Armenian Dram", 32 | "Ccy": "AMD", 33 | "CcyNbr": 51, 34 | "CcyMnrUnts": 2, 35 | "CcyNm/_IsFund": "", 36 | "CcyNm/__text": "" 37 | }, 38 | { 39 | "CtryNm": "CURAÇAO", 40 | "CcyNm": "Netherlands Antillean Guilder", 41 | "Ccy": "ANG", 42 | "CcyNbr": 532, 43 | "CcyMnrUnts": 2, 44 | "CcyNm/_IsFund": "", 45 | "CcyNm/__text": "" 46 | }, 47 | { 48 | "CtryNm": "ANGOLA", 49 | "CcyNm": "Kwanza", 50 | "Ccy": "AOA", 51 | "CcyNbr": 973, 52 | "CcyMnrUnts": 2, 53 | "CcyNm/_IsFund": "", 54 | "CcyNm/__text": "" 55 | }, 56 | { 57 | "CtryNm": "ARGENTINA", 58 | "CcyNm": "Argentine Peso", 59 | "Ccy": "ARS", 60 | "CcyNbr": 32, 61 | "CcyMnrUnts": 2, 62 | "CcyNm/_IsFund": "", 63 | "CcyNm/__text": "" 64 | }, 65 | { 66 | "CtryNm": "AUSTRALIA", 67 | "CcyNm": "Australian Dollar", 68 | "Ccy": "AUD", 69 | "CcyNbr": 36, 70 | "CcyMnrUnts": 2, 71 | "CcyNm/_IsFund": "", 72 | "CcyNm/__text": "" 73 | }, 74 | { 75 | "CtryNm": "ARUBA", 76 | "CcyNm": "Aruban Florin", 77 | "Ccy": "AWG", 78 | "CcyNbr": 533, 79 | "CcyMnrUnts": 2, 80 | "CcyNm/_IsFund": "", 81 | "CcyNm/__text": "" 82 | }, 83 | { 84 | "CtryNm": "AZERBAIJAN", 85 | "CcyNm": "Azerbaijan Manat", 86 | "Ccy": "AZN", 87 | "CcyNbr": 944, 88 | "CcyMnrUnts": 2, 89 | "CcyNm/_IsFund": "", 90 | "CcyNm/__text": "" 91 | }, 92 | { 93 | "CtryNm": "BOSNIA AND HERZEGOVINA", 94 | "CcyNm": "Convertible Mark", 95 | "Ccy": "BAM", 96 | "CcyNbr": 977, 97 | "CcyMnrUnts": 2, 98 | "CcyNm/_IsFund": "", 99 | "CcyNm/__text": "" 100 | }, 101 | { 102 | "CtryNm": "BARBADOS", 103 | "CcyNm": "Barbados Dollar", 104 | "Ccy": "BBD", 105 | "CcyNbr": 52, 106 | "CcyMnrUnts": 2, 107 | "CcyNm/_IsFund": "", 108 | "CcyNm/__text": "" 109 | }, 110 | { 111 | "CtryNm": "BANGLADESH", 112 | "CcyNm": "Taka", 113 | "Ccy": "BDT", 114 | "CcyNbr": 50, 115 | "CcyMnrUnts": 2, 116 | "CcyNm/_IsFund": "", 117 | "CcyNm/__text": "" 118 | }, 119 | { 120 | "CtryNm": "BULGARIA", 121 | "CcyNm": "Bulgarian Lev", 122 | "Ccy": "BGN", 123 | "CcyNbr": 975, 124 | "CcyMnrUnts": 2, 125 | "CcyNm/_IsFund": "", 126 | "CcyNm/__text": "" 127 | }, 128 | { 129 | "CtryNm": "BAHRAIN", 130 | "CcyNm": "Bahraini Dinar", 131 | "Ccy": "BHD", 132 | "CcyNbr": 48, 133 | "CcyMnrUnts": 3, 134 | "CcyNm/_IsFund": "", 135 | "CcyNm/__text": "" 136 | }, 137 | { 138 | "CtryNm": "BURUNDI", 139 | "CcyNm": "Burundi Franc", 140 | "Ccy": "BIF", 141 | "CcyNbr": 108, 142 | "CcyMnrUnts": 0, 143 | "CcyNm/_IsFund": "", 144 | "CcyNm/__text": "" 145 | }, 146 | { 147 | "CtryNm": "BERMUDA", 148 | "CcyNm": "Bermudian Dollar", 149 | "Ccy": "BMD", 150 | "CcyNbr": 60, 151 | "CcyMnrUnts": 2, 152 | "CcyNm/_IsFund": "", 153 | "CcyNm/__text": "" 154 | }, 155 | { 156 | "CtryNm": "BRUNEI DARUSSALAM", 157 | "CcyNm": "Brunei Dollar", 158 | "Ccy": "BND", 159 | "CcyNbr": 96, 160 | "CcyMnrUnts": 2, 161 | "CcyNm/_IsFund": "", 162 | "CcyNm/__text": "" 163 | }, 164 | { 165 | "CtryNm": "BOLIVIA (PLURINATIONAL STATE OF)", 166 | "CcyNm": "Boliviano", 167 | "Ccy": "BOB", 168 | "CcyNbr": 68, 169 | "CcyMnrUnts": 2, 170 | "CcyNm/_IsFund": "", 171 | "CcyNm/__text": "" 172 | }, 173 | { 174 | "CtryNm": "BRAZIL", 175 | "CcyNm": "Brazilian Real", 176 | "Ccy": "BRL", 177 | "CcyNbr": 986, 178 | "CcyMnrUnts": 2, 179 | "CcyNm/_IsFund": "", 180 | "CcyNm/__text": "" 181 | }, 182 | { 183 | "CtryNm": "BAHAMAS (THE)", 184 | "CcyNm": "Bahamian Dollar", 185 | "Ccy": "BSD", 186 | "CcyNbr": 44, 187 | "CcyMnrUnts": 2, 188 | "CcyNm/_IsFund": "", 189 | "CcyNm/__text": "" 190 | }, 191 | { 192 | "CtryNm": "BHUTAN", 193 | "CcyNm": "Ngultrum", 194 | "Ccy": "BTN", 195 | "CcyNbr": 64, 196 | "CcyMnrUnts": 2, 197 | "CcyNm/_IsFund": "", 198 | "CcyNm/__text": "" 199 | }, 200 | { 201 | "CtryNm": "BOTSWANA", 202 | "CcyNm": "Pula", 203 | "Ccy": "BWP", 204 | "CcyNbr": 72, 205 | "CcyMnrUnts": 2, 206 | "CcyNm/_IsFund": "", 207 | "CcyNm/__text": "" 208 | }, 209 | { 210 | "CtryNm": "BELARUS", 211 | "CcyNm": "Belarusian Ruble", 212 | "Ccy": "BYN", 213 | "CcyNbr": 933, 214 | "CcyMnrUnts": 2, 215 | "CcyNm/_IsFund": "", 216 | "CcyNm/__text": "" 217 | }, 218 | { 219 | "CtryNm": "BELIZE", 220 | "CcyNm": "Belize Dollar", 221 | "Ccy": "BZD", 222 | "CcyNbr": 84, 223 | "CcyMnrUnts": 2, 224 | "CcyNm/_IsFund": "", 225 | "CcyNm/__text": "" 226 | }, 227 | { 228 | "CtryNm": "CANADA", 229 | "CcyNm": "Canadian Dollar", 230 | "Ccy": "CAD", 231 | "CcyNbr": 124, 232 | "CcyMnrUnts": 2, 233 | "CcyNm/_IsFund": "", 234 | "CcyNm/__text": "" 235 | }, 236 | { 237 | "CtryNm": "CONGO (THE DEMOCRATIC REPUBLIC OF THE)", 238 | "CcyNm": "Congolese Franc", 239 | "Ccy": "CDF", 240 | "CcyNbr": 976, 241 | "CcyMnrUnts": 2, 242 | "CcyNm/_IsFund": "", 243 | "CcyNm/__text": "" 244 | }, 245 | { 246 | "CtryNm": "SWITZERLAND", 247 | "CcyNm": "Swiss Franc", 248 | "Ccy": "CHF", 249 | "CcyNbr": 756, 250 | "CcyMnrUnts": 2, 251 | "CcyNm/_IsFund": "", 252 | "CcyNm/__text": "" 253 | }, 254 | { 255 | "CtryNm": "CHILE", 256 | "CcyNm": "Chilean Peso", 257 | "Ccy": "CLP", 258 | "CcyNbr": 152, 259 | "CcyMnrUnts": 0, 260 | "CcyNm/_IsFund": "", 261 | "CcyNm/__text": "" 262 | }, 263 | { 264 | "CtryNm": "CHINA", 265 | "CcyNm": "Yuan Renminbi", 266 | "Ccy": "CNY", 267 | "CcyNbr": 156, 268 | "CcyMnrUnts": 2, 269 | "CcyNm/_IsFund": "", 270 | "CcyNm/__text": "" 271 | }, 272 | { 273 | "CtryNm": "COLOMBIA", 274 | "CcyNm": "Colombian Peso", 275 | "Ccy": "COP", 276 | "CcyNbr": 170, 277 | "CcyMnrUnts": 2, 278 | "CcyNm/_IsFund": "", 279 | "CcyNm/__text": "" 280 | }, 281 | { 282 | "CtryNm": "COSTA RICA", 283 | "CcyNm": "Costa Rican Colon", 284 | "Ccy": "CRC", 285 | "CcyNbr": 188, 286 | "CcyMnrUnts": 2, 287 | "CcyNm/_IsFund": "", 288 | "CcyNm/__text": "" 289 | }, 290 | { 291 | "CtryNm": "CUBA", 292 | "CcyNm": "Peso Convertible", 293 | "Ccy": "CUC", 294 | "CcyNbr": 931, 295 | "CcyMnrUnts": 2, 296 | "CcyNm/_IsFund": "", 297 | "CcyNm/__text": "" 298 | }, 299 | { 300 | "CtryNm": "CUBA", 301 | "CcyNm": "Cuban Peso", 302 | "Ccy": "CUP", 303 | "CcyNbr": 192, 304 | "CcyMnrUnts": 2, 305 | "CcyNm/_IsFund": "", 306 | "CcyNm/__text": "" 307 | }, 308 | { 309 | "CtryNm": "CABO VERDE", 310 | "CcyNm": "Cabo Verde Escudo", 311 | "Ccy": "CVE", 312 | "CcyNbr": 132, 313 | "CcyMnrUnts": 2, 314 | "CcyNm/_IsFund": "", 315 | "CcyNm/__text": "" 316 | }, 317 | { 318 | "CtryNm": "CZECHIA", 319 | "CcyNm": "Czech Koruna", 320 | "Ccy": "CZK", 321 | "CcyNbr": 203, 322 | "CcyMnrUnts": 2, 323 | "CcyNm/_IsFund": "", 324 | "CcyNm/__text": "" 325 | }, 326 | { 327 | "CtryNm": "DJIBOUTI", 328 | "CcyNm": "Djibouti Franc", 329 | "Ccy": "DJF", 330 | "CcyNbr": 262, 331 | "CcyMnrUnts": 0, 332 | "CcyNm/_IsFund": "", 333 | "CcyNm/__text": "" 334 | }, 335 | { 336 | "CtryNm": "DENMARK", 337 | "CcyNm": "Danish Krone", 338 | "Ccy": "DKK", 339 | "CcyNbr": 208, 340 | "CcyMnrUnts": 2, 341 | "CcyNm/_IsFund": "", 342 | "CcyNm/__text": "" 343 | }, 344 | { 345 | "CtryNm": "DOMINICAN REPUBLIC (THE)", 346 | "CcyNm": "Dominican Peso", 347 | "Ccy": "DOP", 348 | "CcyNbr": 214, 349 | "CcyMnrUnts": 2, 350 | "CcyNm/_IsFund": "", 351 | "CcyNm/__text": "" 352 | }, 353 | { 354 | "CtryNm": "ALGERIA", 355 | "CcyNm": "Algerian Dinar", 356 | "Ccy": "DZD", 357 | "CcyNbr": 12, 358 | "CcyMnrUnts": 2, 359 | "CcyNm/_IsFund": "", 360 | "CcyNm/__text": "" 361 | }, 362 | { 363 | "CtryNm": "EGYPT", 364 | "CcyNm": "Egyptian Pound", 365 | "Ccy": "EGP", 366 | "CcyNbr": 818, 367 | "CcyMnrUnts": 2, 368 | "CcyNm/_IsFund": "", 369 | "CcyNm/__text": "" 370 | }, 371 | { 372 | "CtryNm": "ERITREA", 373 | "CcyNm": "Nakfa", 374 | "Ccy": "ERN", 375 | "CcyNbr": 232, 376 | "CcyMnrUnts": 2, 377 | "CcyNm/_IsFund": "", 378 | "CcyNm/__text": "" 379 | }, 380 | { 381 | "CtryNm": "ETHIOPIA", 382 | "CcyNm": "Ethiopian Birr", 383 | "Ccy": "ETB", 384 | "CcyNbr": 230, 385 | "CcyMnrUnts": 2, 386 | "CcyNm/_IsFund": "", 387 | "CcyNm/__text": "" 388 | }, 389 | { 390 | "CtryNm": "ÅLAND ISLANDS", 391 | "CcyNm": "Euro", 392 | "Ccy": "EUR", 393 | "CcyNbr": 978, 394 | "CcyMnrUnts": 2, 395 | "CcyNm/_IsFund": "", 396 | "CcyNm/__text": "" 397 | }, 398 | { 399 | "CtryNm": "FIJI", 400 | "CcyNm": "Fiji Dollar", 401 | "Ccy": "FJD", 402 | "CcyNbr": 242, 403 | "CcyMnrUnts": 2, 404 | "CcyNm/_IsFund": "", 405 | "CcyNm/__text": "" 406 | }, 407 | { 408 | "CtryNm": "FALKLAND ISLANDS (THE) [MALVINAS]", 409 | "CcyNm": "Falkland Islands Pound", 410 | "Ccy": "FKP", 411 | "CcyNbr": 238, 412 | "CcyMnrUnts": 2, 413 | "CcyNm/_IsFund": "", 414 | "CcyNm/__text": "" 415 | }, 416 | { 417 | "CtryNm": "UNITED KINGDOM OF GREAT BRITAIN AND NORTHERN IRELAND (THE)", 418 | "CcyNm": "Pound Sterling", 419 | "Ccy": "GBP", 420 | "CcyNbr": 826, 421 | "CcyMnrUnts": 2, 422 | "CcyNm/_IsFund": "", 423 | "CcyNm/__text": "" 424 | }, 425 | { 426 | "CtryNm": "GEORGIA", 427 | "CcyNm": "Lari", 428 | "Ccy": "GEL", 429 | "CcyNbr": 981, 430 | "CcyMnrUnts": 2, 431 | "CcyNm/_IsFund": "", 432 | "CcyNm/__text": "" 433 | }, 434 | { 435 | "CtryNm": "GHANA", 436 | "CcyNm": "Ghana Cedi", 437 | "Ccy": "GHS", 438 | "CcyNbr": 936, 439 | "CcyMnrUnts": 2, 440 | "CcyNm/_IsFund": "", 441 | "CcyNm/__text": "" 442 | }, 443 | { 444 | "CtryNm": "GIBRALTAR", 445 | "CcyNm": "Gibraltar Pound", 446 | "Ccy": "GIP", 447 | "CcyNbr": 292, 448 | "CcyMnrUnts": 2, 449 | "CcyNm/_IsFund": "", 450 | "CcyNm/__text": "" 451 | }, 452 | { 453 | "CtryNm": "GAMBIA (THE)", 454 | "CcyNm": "Dalasi", 455 | "Ccy": "GMD", 456 | "CcyNbr": 270, 457 | "CcyMnrUnts": 2, 458 | "CcyNm/_IsFund": "", 459 | "CcyNm/__text": "" 460 | }, 461 | { 462 | "CtryNm": "GUINEA", 463 | "CcyNm": "Guinean Franc", 464 | "Ccy": "GNF", 465 | "CcyNbr": 324, 466 | "CcyMnrUnts": 0, 467 | "CcyNm/_IsFund": "", 468 | "CcyNm/__text": "" 469 | }, 470 | { 471 | "CtryNm": "GUATEMALA", 472 | "CcyNm": "Quetzal", 473 | "Ccy": "GTQ", 474 | "CcyNbr": 320, 475 | "CcyMnrUnts": 2, 476 | "CcyNm/_IsFund": "", 477 | "CcyNm/__text": "" 478 | }, 479 | { 480 | "CtryNm": "GUYANA", 481 | "CcyNm": "Guyana Dollar", 482 | "Ccy": "GYD", 483 | "CcyNbr": 328, 484 | "CcyMnrUnts": 2, 485 | "CcyNm/_IsFund": "", 486 | "CcyNm/__text": "" 487 | }, 488 | { 489 | "CtryNm": "HONG KONG", 490 | "CcyNm": "Hong Kong Dollar", 491 | "Ccy": "HKD", 492 | "CcyNbr": 344, 493 | "CcyMnrUnts": 2, 494 | "CcyNm/_IsFund": "", 495 | "CcyNm/__text": "" 496 | }, 497 | { 498 | "CtryNm": "HONDURAS", 499 | "CcyNm": "Lempira", 500 | "Ccy": "HNL", 501 | "CcyNbr": 340, 502 | "CcyMnrUnts": 2, 503 | "CcyNm/_IsFund": "", 504 | "CcyNm/__text": "" 505 | }, 506 | { 507 | "CtryNm": "CROATIA", 508 | "CcyNm": "Kuna", 509 | "Ccy": "HRK", 510 | "CcyNbr": 191, 511 | "CcyMnrUnts": 2, 512 | "CcyNm/_IsFund": "", 513 | "CcyNm/__text": "" 514 | }, 515 | { 516 | "CtryNm": "HAITI", 517 | "CcyNm": "Gourde", 518 | "Ccy": "HTG", 519 | "CcyNbr": 332, 520 | "CcyMnrUnts": 2, 521 | "CcyNm/_IsFund": "", 522 | "CcyNm/__text": "" 523 | }, 524 | { 525 | "CtryNm": "HUNGARY", 526 | "CcyNm": "Forint", 527 | "Ccy": "HUF", 528 | "CcyNbr": 348, 529 | "CcyMnrUnts": 2, 530 | "CcyNm/_IsFund": "", 531 | "CcyNm/__text": "" 532 | }, 533 | { 534 | "CtryNm": "INDONESIA", 535 | "CcyNm": "Rupiah", 536 | "Ccy": "IDR", 537 | "CcyNbr": 360, 538 | "CcyMnrUnts": 2, 539 | "CcyNm/_IsFund": "", 540 | "CcyNm/__text": "" 541 | }, 542 | { 543 | "CtryNm": "ISRAEL", 544 | "CcyNm": "New Israeli Sheqel", 545 | "Ccy": "ILS", 546 | "CcyNbr": 376, 547 | "CcyMnrUnts": 2, 548 | "CcyNm/_IsFund": "", 549 | "CcyNm/__text": "" 550 | }, 551 | { 552 | "CtryNm": "INDIA", 553 | "CcyNm": "Indian Rupee", 554 | "Ccy": "INR", 555 | "CcyNbr": 356, 556 | "CcyMnrUnts": 2, 557 | "CcyNm/_IsFund": "", 558 | "CcyNm/__text": "" 559 | }, 560 | { 561 | "CtryNm": "IRAQ", 562 | "CcyNm": "Iraqi Dinar", 563 | "Ccy": "IQD", 564 | "CcyNbr": 368, 565 | "CcyMnrUnts": 3, 566 | "CcyNm/_IsFund": "", 567 | "CcyNm/__text": "" 568 | }, 569 | { 570 | "CtryNm": "IRAN (ISLAMIC REPUBLIC OF)", 571 | "CcyNm": "Iranian Rial", 572 | "Ccy": "IRR", 573 | "CcyNbr": 364, 574 | "CcyMnrUnts": 2, 575 | "CcyNm/_IsFund": "", 576 | "CcyNm/__text": "" 577 | }, 578 | { 579 | "CtryNm": "ICELAND", 580 | "CcyNm": "Iceland Krona", 581 | "Ccy": "ISK", 582 | "CcyNbr": 352, 583 | "CcyMnrUnts": 0, 584 | "CcyNm/_IsFund": "", 585 | "CcyNm/__text": "" 586 | }, 587 | { 588 | "CtryNm": "JAMAICA", 589 | "CcyNm": "Jamaican Dollar", 590 | "Ccy": "JMD", 591 | "CcyNbr": 388, 592 | "CcyMnrUnts": 2, 593 | "CcyNm/_IsFund": "", 594 | "CcyNm/__text": "" 595 | }, 596 | { 597 | "CtryNm": "JORDAN", 598 | "CcyNm": "Jordanian Dinar", 599 | "Ccy": "JOD", 600 | "CcyNbr": 400, 601 | "CcyMnrUnts": 3, 602 | "CcyNm/_IsFund": "", 603 | "CcyNm/__text": "" 604 | }, 605 | { 606 | "CtryNm": "JAPAN", 607 | "CcyNm": "Yen", 608 | "Ccy": "JPY", 609 | "CcyNbr": 392, 610 | "CcyMnrUnts": 0, 611 | "CcyNm/_IsFund": "", 612 | "CcyNm/__text": "" 613 | }, 614 | { 615 | "CtryNm": "KENYA", 616 | "CcyNm": "Kenyan Shilling", 617 | "Ccy": "KES", 618 | "CcyNbr": 404, 619 | "CcyMnrUnts": 2, 620 | "CcyNm/_IsFund": "", 621 | "CcyNm/__text": "" 622 | }, 623 | { 624 | "CtryNm": "KYRGYZSTAN", 625 | "CcyNm": "Som", 626 | "Ccy": "KGS", 627 | "CcyNbr": 417, 628 | "CcyMnrUnts": 2, 629 | "CcyNm/_IsFund": "", 630 | "CcyNm/__text": "" 631 | }, 632 | { 633 | "CtryNm": "CAMBODIA", 634 | "CcyNm": "Riel", 635 | "Ccy": "KHR", 636 | "CcyNbr": 116, 637 | "CcyMnrUnts": 2, 638 | "CcyNm/_IsFund": "", 639 | "CcyNm/__text": "" 640 | }, 641 | { 642 | "CtryNm": "COMOROS (THE)", 643 | "CcyNm": "Comorian Franc", 644 | "Ccy": "KMF", 645 | "CcyNbr": 174, 646 | "CcyMnrUnts": 0, 647 | "CcyNm/_IsFund": "", 648 | "CcyNm/__text": "" 649 | }, 650 | { 651 | "CtryNm": "KOREA (THE DEMOCRATIC PEOPLE’S REPUBLIC OF)", 652 | "CcyNm": "North Korean Won", 653 | "Ccy": "KPW", 654 | "CcyNbr": 408, 655 | "CcyMnrUnts": 2, 656 | "CcyNm/_IsFund": "", 657 | "CcyNm/__text": "" 658 | }, 659 | { 660 | "CtryNm": "KOREA (THE REPUBLIC OF)", 661 | "CcyNm": "Won", 662 | "Ccy": "KRW", 663 | "CcyNbr": 410, 664 | "CcyMnrUnts": 0, 665 | "CcyNm/_IsFund": "", 666 | "CcyNm/__text": "" 667 | }, 668 | { 669 | "CtryNm": "KUWAIT", 670 | "CcyNm": "Kuwaiti Dinar", 671 | "Ccy": "KWD", 672 | "CcyNbr": 414, 673 | "CcyMnrUnts": 3, 674 | "CcyNm/_IsFund": "", 675 | "CcyNm/__text": "" 676 | }, 677 | { 678 | "CtryNm": "CAYMAN ISLANDS (THE)", 679 | "CcyNm": "Cayman Islands Dollar", 680 | "Ccy": "KYD", 681 | "CcyNbr": 136, 682 | "CcyMnrUnts": 2, 683 | "CcyNm/_IsFund": "", 684 | "CcyNm/__text": "" 685 | }, 686 | { 687 | "CtryNm": "KAZAKHSTAN", 688 | "CcyNm": "Tenge", 689 | "Ccy": "KZT", 690 | "CcyNbr": 398, 691 | "CcyMnrUnts": 2, 692 | "CcyNm/_IsFund": "", 693 | "CcyNm/__text": "" 694 | }, 695 | { 696 | "CtryNm": "LAO PEOPLE’S DEMOCRATIC REPUBLIC (THE)", 697 | "CcyNm": "Lao Kip", 698 | "Ccy": "LAK", 699 | "CcyNbr": 418, 700 | "CcyMnrUnts": 2, 701 | "CcyNm/_IsFund": "", 702 | "CcyNm/__text": "" 703 | }, 704 | { 705 | "CtryNm": "LEBANON", 706 | "CcyNm": "Lebanese Pound", 707 | "Ccy": "LBP", 708 | "CcyNbr": 422, 709 | "CcyMnrUnts": 2, 710 | "CcyNm/_IsFund": "", 711 | "CcyNm/__text": "" 712 | }, 713 | { 714 | "CtryNm": "SRI LANKA", 715 | "CcyNm": "Sri Lanka Rupee", 716 | "Ccy": "LKR", 717 | "CcyNbr": 144, 718 | "CcyMnrUnts": 2, 719 | "CcyNm/_IsFund": "", 720 | "CcyNm/__text": "" 721 | }, 722 | { 723 | "CtryNm": "LIBERIA", 724 | "CcyNm": "Liberian Dollar", 725 | "Ccy": "LRD", 726 | "CcyNbr": 430, 727 | "CcyMnrUnts": 2, 728 | "CcyNm/_IsFund": "", 729 | "CcyNm/__text": "" 730 | }, 731 | { 732 | "CtryNm": "LESOTHO", 733 | "CcyNm": "Loti", 734 | "Ccy": "LSL", 735 | "CcyNbr": 426, 736 | "CcyMnrUnts": 2, 737 | "CcyNm/_IsFund": "", 738 | "CcyNm/__text": "" 739 | }, 740 | { 741 | "CtryNm": "LIBYA", 742 | "CcyNm": "Libyan Dinar", 743 | "Ccy": "LYD", 744 | "CcyNbr": 434, 745 | "CcyMnrUnts": 3, 746 | "CcyNm/_IsFund": "", 747 | "CcyNm/__text": "" 748 | }, 749 | { 750 | "CtryNm": "MOROCCO", 751 | "CcyNm": "Moroccan Dirham", 752 | "Ccy": "MAD", 753 | "CcyNbr": 504, 754 | "CcyMnrUnts": 2, 755 | "CcyNm/_IsFund": "", 756 | "CcyNm/__text": "" 757 | }, 758 | { 759 | "CtryNm": "MOLDOVA (THE REPUBLIC OF)", 760 | "CcyNm": "Moldovan Leu", 761 | "Ccy": "MDL", 762 | "CcyNbr": 498, 763 | "CcyMnrUnts": 2, 764 | "CcyNm/_IsFund": "", 765 | "CcyNm/__text": "" 766 | }, 767 | { 768 | "CtryNm": "MADAGASCAR", 769 | "CcyNm": "Malagasy Ariary", 770 | "Ccy": "MGA", 771 | "CcyNbr": 969, 772 | "CcyMnrUnts": 2, 773 | "CcyNm/_IsFund": "", 774 | "CcyNm/__text": "" 775 | }, 776 | { 777 | "CtryNm": "MACEDONIA (THE FORMER YUGOSLAV REPUBLIC OF)", 778 | "CcyNm": "Denar", 779 | "Ccy": "MKD", 780 | "CcyNbr": 807, 781 | "CcyMnrUnts": 2, 782 | "CcyNm/_IsFund": "", 783 | "CcyNm/__text": "" 784 | }, 785 | { 786 | "CtryNm": "MYANMAR", 787 | "CcyNm": "Kyat", 788 | "Ccy": "MMK", 789 | "CcyNbr": 104, 790 | "CcyMnrUnts": 2, 791 | "CcyNm/_IsFund": "", 792 | "CcyNm/__text": "" 793 | }, 794 | { 795 | "CtryNm": "MONGOLIA", 796 | "CcyNm": "Tugrik", 797 | "Ccy": "MNT", 798 | "CcyNbr": 496, 799 | "CcyMnrUnts": 2, 800 | "CcyNm/_IsFund": "", 801 | "CcyNm/__text": "" 802 | }, 803 | { 804 | "CtryNm": "MACAO", 805 | "CcyNm": "Pataca", 806 | "Ccy": "MOP", 807 | "CcyNbr": 446, 808 | "CcyMnrUnts": 2, 809 | "CcyNm/_IsFund": "", 810 | "CcyNm/__text": "" 811 | }, 812 | { 813 | "CtryNm": "MAURITANIA", 814 | "CcyNm": "Ouguiya", 815 | "Ccy": "MRU", 816 | "CcyNbr": 929, 817 | "CcyMnrUnts": 2, 818 | "CcyNm/_IsFund": "", 819 | "CcyNm/__text": "" 820 | }, 821 | { 822 | "CtryNm": "MAURITIUS", 823 | "CcyNm": "Mauritius Rupee", 824 | "Ccy": "MUR", 825 | "CcyNbr": 480, 826 | "CcyMnrUnts": 2, 827 | "CcyNm/_IsFund": "", 828 | "CcyNm/__text": "" 829 | }, 830 | { 831 | "CtryNm": "MALDIVES", 832 | "CcyNm": "Rufiyaa", 833 | "Ccy": "MVR", 834 | "CcyNbr": 462, 835 | "CcyMnrUnts": 2, 836 | "CcyNm/_IsFund": "", 837 | "CcyNm/__text": "" 838 | }, 839 | { 840 | "CtryNm": "MALAWI", 841 | "CcyNm": "Malawi Kwacha", 842 | "Ccy": "MWK", 843 | "CcyNbr": 454, 844 | "CcyMnrUnts": 2, 845 | "CcyNm/_IsFund": "", 846 | "CcyNm/__text": "" 847 | }, 848 | { 849 | "CtryNm": "MEXICO", 850 | "CcyNm": "Mexican Peso", 851 | "Ccy": "MXN", 852 | "CcyNbr": 484, 853 | "CcyMnrUnts": 2, 854 | "CcyNm/_IsFund": "", 855 | "CcyNm/__text": "" 856 | }, 857 | { 858 | "CtryNm": "MALAYSIA", 859 | "CcyNm": "Malaysian Ringgit", 860 | "Ccy": "MYR", 861 | "CcyNbr": 458, 862 | "CcyMnrUnts": 2, 863 | "CcyNm/_IsFund": "", 864 | "CcyNm/__text": "" 865 | }, 866 | { 867 | "CtryNm": "MOZAMBIQUE", 868 | "CcyNm": "Mozambique Metical", 869 | "Ccy": "MZN", 870 | "CcyNbr": 943, 871 | "CcyMnrUnts": 2, 872 | "CcyNm/_IsFund": "", 873 | "CcyNm/__text": "" 874 | }, 875 | { 876 | "CtryNm": "NAMIBIA", 877 | "CcyNm": "Namibia Dollar", 878 | "Ccy": "NAD", 879 | "CcyNbr": 516, 880 | "CcyMnrUnts": 2, 881 | "CcyNm/_IsFund": "", 882 | "CcyNm/__text": "" 883 | }, 884 | { 885 | "CtryNm": "NIGERIA", 886 | "CcyNm": "Naira", 887 | "Ccy": "NGN", 888 | "CcyNbr": 566, 889 | "CcyMnrUnts": 2, 890 | "CcyNm/_IsFund": "", 891 | "CcyNm/__text": "" 892 | }, 893 | { 894 | "CtryNm": "NICARAGUA", 895 | "CcyNm": "Cordoba Oro", 896 | "Ccy": "NIO", 897 | "CcyNbr": 558, 898 | "CcyMnrUnts": 2, 899 | "CcyNm/_IsFund": "", 900 | "CcyNm/__text": "" 901 | }, 902 | { 903 | "CtryNm": "NORWAY", 904 | "CcyNm": "Norwegian Krone", 905 | "Ccy": "NOK", 906 | "CcyNbr": 578, 907 | "CcyMnrUnts": 2, 908 | "CcyNm/_IsFund": "", 909 | "CcyNm/__text": "" 910 | }, 911 | { 912 | "CtryNm": "NEPAL", 913 | "CcyNm": "Nepalese Rupee", 914 | "Ccy": "NPR", 915 | "CcyNbr": 524, 916 | "CcyMnrUnts": 2, 917 | "CcyNm/_IsFund": "", 918 | "CcyNm/__text": "" 919 | }, 920 | { 921 | "CtryNm": "NEW ZEALAND", 922 | "CcyNm": "New Zealand Dollar", 923 | "Ccy": "NZD", 924 | "CcyNbr": 554, 925 | "CcyMnrUnts": 2, 926 | "CcyNm/_IsFund": "", 927 | "CcyNm/__text": "" 928 | }, 929 | { 930 | "CtryNm": "OMAN", 931 | "CcyNm": "Rial Omani", 932 | "Ccy": "OMR", 933 | "CcyNbr": 512, 934 | "CcyMnrUnts": 3, 935 | "CcyNm/_IsFund": "", 936 | "CcyNm/__text": "" 937 | }, 938 | { 939 | "CtryNm": "PANAMA", 940 | "CcyNm": "Balboa", 941 | "Ccy": "PAB", 942 | "CcyNbr": 590, 943 | "CcyMnrUnts": 2, 944 | "CcyNm/_IsFund": "", 945 | "CcyNm/__text": "" 946 | }, 947 | { 948 | "CtryNm": "PERU", 949 | "CcyNm": "Sol", 950 | "Ccy": "PEN", 951 | "CcyNbr": 604, 952 | "CcyMnrUnts": 2, 953 | "CcyNm/_IsFund": "", 954 | "CcyNm/__text": "" 955 | }, 956 | { 957 | "CtryNm": "PAPUA NEW GUINEA", 958 | "CcyNm": "Kina", 959 | "Ccy": "PGK", 960 | "CcyNbr": 598, 961 | "CcyMnrUnts": 2, 962 | "CcyNm/_IsFund": "", 963 | "CcyNm/__text": "" 964 | }, 965 | { 966 | "CtryNm": "PHILIPPINES (THE)", 967 | "CcyNm": "Philippine Peso", 968 | "Ccy": "PHP", 969 | "CcyNbr": 608, 970 | "CcyMnrUnts": 2, 971 | "CcyNm/_IsFund": "", 972 | "CcyNm/__text": "" 973 | }, 974 | { 975 | "CtryNm": "PAKISTAN", 976 | "CcyNm": "Pakistan Rupee", 977 | "Ccy": "PKR", 978 | "CcyNbr": 586, 979 | "CcyMnrUnts": 2, 980 | "CcyNm/_IsFund": "", 981 | "CcyNm/__text": "" 982 | }, 983 | { 984 | "CtryNm": "POLAND", 985 | "CcyNm": "Zloty", 986 | "Ccy": "PLN", 987 | "CcyNbr": 985, 988 | "CcyMnrUnts": 2, 989 | "CcyNm/_IsFund": "", 990 | "CcyNm/__text": "" 991 | }, 992 | { 993 | "CtryNm": "PARAGUAY", 994 | "CcyNm": "Guarani", 995 | "Ccy": "PYG", 996 | "CcyNbr": 600, 997 | "CcyMnrUnts": 0, 998 | "CcyNm/_IsFund": "", 999 | "CcyNm/__text": "" 1000 | }, 1001 | { 1002 | "CtryNm": "QATAR", 1003 | "CcyNm": "Qatari Rial", 1004 | "Ccy": "QAR", 1005 | "CcyNbr": 634, 1006 | "CcyMnrUnts": 2, 1007 | "CcyNm/_IsFund": "", 1008 | "CcyNm/__text": "" 1009 | }, 1010 | { 1011 | "CtryNm": "ROMANIA", 1012 | "CcyNm": "Romanian Leu", 1013 | "Ccy": "RON", 1014 | "CcyNbr": 946, 1015 | "CcyMnrUnts": 2, 1016 | "CcyNm/_IsFund": "", 1017 | "CcyNm/__text": "" 1018 | }, 1019 | { 1020 | "CtryNm": "SERBIA", 1021 | "CcyNm": "Serbian Dinar", 1022 | "Ccy": "RSD", 1023 | "CcyNbr": 941, 1024 | "CcyMnrUnts": 2, 1025 | "CcyNm/_IsFund": "", 1026 | "CcyNm/__text": "" 1027 | }, 1028 | { 1029 | "CtryNm": "RUSSIAN FEDERATION (THE)", 1030 | "CcyNm": "Russian Ruble", 1031 | "Ccy": "RUB", 1032 | "CcyNbr": 643, 1033 | "CcyMnrUnts": 2, 1034 | "CcyNm/_IsFund": "", 1035 | "CcyNm/__text": "" 1036 | }, 1037 | { 1038 | "CtryNm": "RWANDA", 1039 | "CcyNm": "Rwanda Franc", 1040 | "Ccy": "RWF", 1041 | "CcyNbr": 646, 1042 | "CcyMnrUnts": 0, 1043 | "CcyNm/_IsFund": "", 1044 | "CcyNm/__text": "" 1045 | }, 1046 | { 1047 | "CtryNm": "SAUDI ARABIA", 1048 | "CcyNm": "Saudi Riyal", 1049 | "Ccy": "SAR", 1050 | "CcyNbr": 682, 1051 | "CcyMnrUnts": 2, 1052 | "CcyNm/_IsFund": "", 1053 | "CcyNm/__text": "" 1054 | }, 1055 | { 1056 | "CtryNm": "SOLOMON ISLANDS", 1057 | "CcyNm": "Solomon Islands Dollar", 1058 | "Ccy": "SBD", 1059 | "CcyNbr": 90, 1060 | "CcyMnrUnts": 2, 1061 | "CcyNm/_IsFund": "", 1062 | "CcyNm/__text": "" 1063 | }, 1064 | { 1065 | "CtryNm": "SEYCHELLES", 1066 | "CcyNm": "Seychelles Rupee", 1067 | "Ccy": "SCR", 1068 | "CcyNbr": 690, 1069 | "CcyMnrUnts": 2, 1070 | "CcyNm/_IsFund": "", 1071 | "CcyNm/__text": "" 1072 | }, 1073 | { 1074 | "CtryNm": "SUDAN (THE)", 1075 | "CcyNm": "Sudanese Pound", 1076 | "Ccy": "SDG", 1077 | "CcyNbr": 938, 1078 | "CcyMnrUnts": 2, 1079 | "CcyNm/_IsFund": "", 1080 | "CcyNm/__text": "" 1081 | }, 1082 | { 1083 | "CtryNm": "SWEDEN", 1084 | "CcyNm": "Swedish Krona", 1085 | "Ccy": "SEK", 1086 | "CcyNbr": 752, 1087 | "CcyMnrUnts": 2, 1088 | "CcyNm/_IsFund": "", 1089 | "CcyNm/__text": "" 1090 | }, 1091 | { 1092 | "CtryNm": "SINGAPORE", 1093 | "CcyNm": "Singapore Dollar", 1094 | "Ccy": "SGD", 1095 | "CcyNbr": 702, 1096 | "CcyMnrUnts": 2, 1097 | "CcyNm/_IsFund": "", 1098 | "CcyNm/__text": "" 1099 | }, 1100 | { 1101 | "CtryNm": "SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA", 1102 | "CcyNm": "Saint Helena Pound", 1103 | "Ccy": "SHP", 1104 | "CcyNbr": 654, 1105 | "CcyMnrUnts": 2, 1106 | "CcyNm/_IsFund": "", 1107 | "CcyNm/__text": "" 1108 | }, 1109 | { 1110 | "CtryNm": "SIERRA LEONE", 1111 | "CcyNm": "Leone", 1112 | "Ccy": "SLL", 1113 | "CcyNbr": 694, 1114 | "CcyMnrUnts": 2, 1115 | "CcyNm/_IsFund": "", 1116 | "CcyNm/__text": "" 1117 | }, 1118 | { 1119 | "CtryNm": "SOMALIA", 1120 | "CcyNm": "Somali Shilling", 1121 | "Ccy": "SOS", 1122 | "CcyNbr": 706, 1123 | "CcyMnrUnts": 2, 1124 | "CcyNm/_IsFund": "", 1125 | "CcyNm/__text": "" 1126 | }, 1127 | { 1128 | "CtryNm": "SURINAME", 1129 | "CcyNm": "Surinam Dollar", 1130 | "Ccy": "SRD", 1131 | "CcyNbr": 968, 1132 | "CcyMnrUnts": 2, 1133 | "CcyNm/_IsFund": "", 1134 | "CcyNm/__text": "" 1135 | }, 1136 | { 1137 | "CtryNm": "SOUTH SUDAN", 1138 | "CcyNm": "South Sudanese Pound", 1139 | "Ccy": "SSP", 1140 | "CcyNbr": 728, 1141 | "CcyMnrUnts": 2, 1142 | "CcyNm/_IsFund": "", 1143 | "CcyNm/__text": "" 1144 | }, 1145 | { 1146 | "CtryNm": "SAO TOME AND PRINCIPE", 1147 | "CcyNm": "Dobra", 1148 | "Ccy": "STN", 1149 | "CcyNbr": 930, 1150 | "CcyMnrUnts": 2, 1151 | "CcyNm/_IsFund": "", 1152 | "CcyNm/__text": "" 1153 | }, 1154 | { 1155 | "CtryNm": "EL SALVADOR", 1156 | "CcyNm": "El Salvador Colon", 1157 | "Ccy": "SVC", 1158 | "CcyNbr": 222, 1159 | "CcyMnrUnts": 2, 1160 | "CcyNm/_IsFund": "", 1161 | "CcyNm/__text": "" 1162 | }, 1163 | { 1164 | "CtryNm": "SYRIAN ARAB REPUBLIC", 1165 | "CcyNm": "Syrian Pound", 1166 | "Ccy": "SYP", 1167 | "CcyNbr": 760, 1168 | "CcyMnrUnts": 2, 1169 | "CcyNm/_IsFund": "", 1170 | "CcyNm/__text": "" 1171 | }, 1172 | { 1173 | "CtryNm": "ESWATINI", 1174 | "CcyNm": "Lilangeni", 1175 | "Ccy": "SZL", 1176 | "CcyNbr": 748, 1177 | "CcyMnrUnts": 2, 1178 | "CcyNm/_IsFund": "", 1179 | "CcyNm/__text": "" 1180 | }, 1181 | { 1182 | "CtryNm": "THAILAND", 1183 | "CcyNm": "Baht", 1184 | "Ccy": "THB", 1185 | "CcyNbr": 764, 1186 | "CcyMnrUnts": 2, 1187 | "CcyNm/_IsFund": "", 1188 | "CcyNm/__text": "" 1189 | }, 1190 | { 1191 | "CtryNm": "TAJIKISTAN", 1192 | "CcyNm": "Somoni", 1193 | "Ccy": "TJS", 1194 | "CcyNbr": 972, 1195 | "CcyMnrUnts": 2, 1196 | "CcyNm/_IsFund": "", 1197 | "CcyNm/__text": "" 1198 | }, 1199 | { 1200 | "CtryNm": "TURKMENISTAN", 1201 | "CcyNm": "Turkmenistan New Manat", 1202 | "Ccy": "TMT", 1203 | "CcyNbr": 934, 1204 | "CcyMnrUnts": 2, 1205 | "CcyNm/_IsFund": "", 1206 | "CcyNm/__text": "" 1207 | }, 1208 | { 1209 | "CtryNm": "TUNISIA", 1210 | "CcyNm": "Tunisian Dinar", 1211 | "Ccy": "TND", 1212 | "CcyNbr": 788, 1213 | "CcyMnrUnts": 3, 1214 | "CcyNm/_IsFund": "", 1215 | "CcyNm/__text": "" 1216 | }, 1217 | { 1218 | "CtryNm": "TONGA", 1219 | "CcyNm": "Pa’anga", 1220 | "Ccy": "TOP", 1221 | "CcyNbr": 776, 1222 | "CcyMnrUnts": 2, 1223 | "CcyNm/_IsFund": "", 1224 | "CcyNm/__text": "" 1225 | }, 1226 | { 1227 | "CtryNm": "TURKEY", 1228 | "CcyNm": "Turkish Lira", 1229 | "Ccy": "TRY", 1230 | "CcyNbr": 949, 1231 | "CcyMnrUnts": 2, 1232 | "CcyNm/_IsFund": "", 1233 | "CcyNm/__text": "" 1234 | }, 1235 | { 1236 | "CtryNm": "TRINIDAD AND TOBAGO", 1237 | "CcyNm": "Trinidad and Tobago Dollar", 1238 | "Ccy": "TTD", 1239 | "CcyNbr": 780, 1240 | "CcyMnrUnts": 2, 1241 | "CcyNm/_IsFund": "", 1242 | "CcyNm/__text": "" 1243 | }, 1244 | { 1245 | "CtryNm": "TAIWAN (PROVINCE OF CHINA)", 1246 | "CcyNm": "New Taiwan Dollar", 1247 | "Ccy": "TWD", 1248 | "CcyNbr": 901, 1249 | "CcyMnrUnts": 2, 1250 | "CcyNm/_IsFund": "", 1251 | "CcyNm/__text": "" 1252 | }, 1253 | { 1254 | "CtryNm": "TANZANIA, UNITED REPUBLIC OF", 1255 | "CcyNm": "Tanzanian Shilling", 1256 | "Ccy": "TZS", 1257 | "CcyNbr": 834, 1258 | "CcyMnrUnts": 2, 1259 | "CcyNm/_IsFund": "", 1260 | "CcyNm/__text": "" 1261 | }, 1262 | { 1263 | "CtryNm": "UKRAINE", 1264 | "CcyNm": "Hryvnia", 1265 | "Ccy": "UAH", 1266 | "CcyNbr": 980, 1267 | "CcyMnrUnts": 2, 1268 | "CcyNm/_IsFund": "", 1269 | "CcyNm/__text": "" 1270 | }, 1271 | { 1272 | "CtryNm": "UGANDA", 1273 | "CcyNm": "Uganda Shilling", 1274 | "Ccy": "UGX", 1275 | "CcyNbr": 800, 1276 | "CcyMnrUnts": 0, 1277 | "CcyNm/_IsFund": "", 1278 | "CcyNm/__text": "" 1279 | }, 1280 | { 1281 | "CtryNm": "UNITED STATES OF AMERICA (THE)", 1282 | "CcyNm": "US Dollar", 1283 | "Ccy": "USD", 1284 | "CcyNbr": 840, 1285 | "CcyMnrUnts": 2, 1286 | "CcyNm/_IsFund": "", 1287 | "CcyNm/__text": "" 1288 | }, 1289 | { 1290 | "CtryNm": "URUGUAY", 1291 | "CcyNm": "Peso Uruguayo", 1292 | "Ccy": "UYU", 1293 | "CcyNbr": 858, 1294 | "CcyMnrUnts": 2, 1295 | "CcyNm/_IsFund": "", 1296 | "CcyNm/__text": "" 1297 | }, 1298 | { 1299 | "CtryNm": "URUGUAY", 1300 | "CcyNm": "Unidad Previsional", 1301 | "Ccy": "UYW", 1302 | "CcyNbr": 927, 1303 | "CcyMnrUnts": 4, 1304 | "CcyNm/_IsFund": "", 1305 | "CcyNm/__text": "" 1306 | }, 1307 | { 1308 | "CtryNm": "UZBEKISTAN", 1309 | "CcyNm": "Uzbekistan Sum", 1310 | "Ccy": "UZS", 1311 | "CcyNbr": 860, 1312 | "CcyMnrUnts": 2, 1313 | "CcyNm/_IsFund": "", 1314 | "CcyNm/__text": "" 1315 | }, 1316 | { 1317 | "CtryNm": "VENEZUELA (BOLIVARIAN REPUBLIC OF)", 1318 | "CcyNm": "Bolívar Soberano", 1319 | "Ccy": "VES", 1320 | "CcyNbr": 928, 1321 | "CcyMnrUnts": 2, 1322 | "CcyNm/_IsFund": "", 1323 | "CcyNm/__text": "" 1324 | }, 1325 | { 1326 | "CtryNm": "VIET NAM", 1327 | "CcyNm": "Dong", 1328 | "Ccy": "VND", 1329 | "CcyNbr": 704, 1330 | "CcyMnrUnts": 0, 1331 | "CcyNm/_IsFund": "", 1332 | "CcyNm/__text": "" 1333 | }, 1334 | { 1335 | "CtryNm": "VANUATU", 1336 | "CcyNm": "Vatu", 1337 | "Ccy": "VUV", 1338 | "CcyNbr": 548, 1339 | "CcyMnrUnts": 0, 1340 | "CcyNm/_IsFund": "", 1341 | "CcyNm/__text": "" 1342 | }, 1343 | { 1344 | "CtryNm": "SAMOA", 1345 | "CcyNm": "Tala", 1346 | "Ccy": "WST", 1347 | "CcyNbr": 882, 1348 | "CcyMnrUnts": 2, 1349 | "CcyNm/_IsFund": "", 1350 | "CcyNm/__text": "" 1351 | }, 1352 | { 1353 | "CtryNm": "CAMEROON", 1354 | "CcyNm": "CFA Franc BEAC", 1355 | "Ccy": "XAF", 1356 | "CcyNbr": 950, 1357 | "CcyMnrUnts": 0, 1358 | "CcyNm/_IsFund": "", 1359 | "CcyNm/__text": "" 1360 | }, 1361 | { 1362 | "CtryNm": "ANGUILLA", 1363 | "CcyNm": "East Caribbean Dollar", 1364 | "Ccy": "XCD", 1365 | "CcyNbr": 951, 1366 | "CcyMnrUnts": 2, 1367 | "CcyNm/_IsFund": "", 1368 | "CcyNm/__text": "" 1369 | }, 1370 | { 1371 | "CtryNm": "BENIN", 1372 | "CcyNm": "CFA Franc BCEAO", 1373 | "Ccy": "XOF", 1374 | "CcyNbr": 952, 1375 | "CcyMnrUnts": 0, 1376 | "CcyNm/_IsFund": "", 1377 | "CcyNm/__text": "" 1378 | }, 1379 | { 1380 | "CtryNm": "FRENCH POLYNESIA", 1381 | "CcyNm": "CFP Franc", 1382 | "Ccy": "XPF", 1383 | "CcyNbr": 953, 1384 | "CcyMnrUnts": 0, 1385 | "CcyNm/_IsFund": "", 1386 | "CcyNm/__text": "" 1387 | }, 1388 | { 1389 | "CtryNm": "YEMEN", 1390 | "CcyNm": "Yemeni Rial", 1391 | "Ccy": "YER", 1392 | "CcyNbr": 886, 1393 | "CcyMnrUnts": 2, 1394 | "CcyNm/_IsFund": "", 1395 | "CcyNm/__text": "" 1396 | }, 1397 | { 1398 | "CtryNm": "LESOTHO", 1399 | "CcyNm": "Rand", 1400 | "Ccy": "ZAR", 1401 | "CcyNbr": 710, 1402 | "CcyMnrUnts": 2, 1403 | "CcyNm/_IsFund": "", 1404 | "CcyNm/__text": "" 1405 | }, 1406 | { 1407 | "CtryNm": "ZAMBIA", 1408 | "CcyNm": "Zambian Kwacha", 1409 | "Ccy": "ZMW", 1410 | "CcyNbr": 967, 1411 | "CcyMnrUnts": 2, 1412 | "CcyNm/_IsFund": "", 1413 | "CcyNm/__text": "" 1414 | }, 1415 | { 1416 | "CtryNm": "ZIMBABWE", 1417 | "CcyNm": "Zimbabwe Dollar", 1418 | "Ccy": "ZWL", 1419 | "CcyNbr": 932, 1420 | "CcyMnrUnts": 2, 1421 | "CcyNm/_IsFund": "", 1422 | "CcyNm/__text": "" 1423 | }, 1424 | { 1425 | "CtryNm": "", 1426 | "CcyNm": "Code reserved for testing", 1427 | "Ccy": "XTS", 1428 | "CcyNbr": 963, 1429 | "CcyMnrUnts": 1, 1430 | "CcyNm/_IsFund": "", 1431 | "CcyNm/__text": "" 1432 | }, 1433 | { 1434 | "CtryNm": "", 1435 | "CcyNm": "No currency", 1436 | "Ccy": "XXX", 1437 | "CcyNbr": 999, 1438 | "CcyMnrUnts": 0, 1439 | "CcyNm/_IsFund": "", 1440 | "CcyNm/__text": "" 1441 | } 1442 | ] -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Peek Travel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The SwiftCurrency Project 3 | ========================= 4 | 5 | Please visit the SwiftCurrency web site for more information: 6 | 7 | * https://github.com/peek-travel/swift-currency 8 | 9 | Copyright 2020 The SwiftCurrency Project 10 | 11 | The SwiftCurrency Project licenses this file to you under the MIT License, (the "License"); you may not use this file except in compliance 12 | with the License. You may obtain a copy of the License at: 13 | 14 | https://opensource.org/licenses/MIT 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 18 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 19 | License for the specific language governing permissions and limitations 20 | under the License. 21 | 22 | Also, please refer to each LICENSE..txt file, which is located in 23 | the 'license' directory of the distribution file, for the license terms of the 24 | components that this product depends on. 25 | 26 | ------------------------------------------------------------------------------- 27 | 28 | This product was heavily influenced by implementations from Flight-School/Money and danthorpe/Money projects. 29 | 30 | * danthorpe/Money 31 | * LICENSE (MIT) 32 | * https://github.com/danthorpe/Money/blob/f44c868dfbeefd612511f2b3a75e6de1bd959ff7/LICENSE 33 | * HOMEPAGE: 34 | * https://github.com/danthorpe/Money/tree/f44c868dfbeefd612511f2b3a75e6de1bd959ff7 35 | * Flight-School/Money 36 | * LICENSE (MIT) 37 | * https://github.com/Flight-School/Money/blob/7f7bb1aee8367376bac34ff08f9b2b732bf553e3/LICENSE.md 38 | * HOMEPAGE: 39 | * https://github.com/Flight-School/Money/tree/7f7bb1aee8367376bac34ff08f9b2b732bf553e3 40 | 41 | --- 42 | 43 | This product contains the derivations of various scripts from Apple's SwiftNIO. 44 | 45 | * LICENSE (Apache License 2.0): 46 | * https://github.com/apple/swift-nio/blob/master/LICENSE.txt 47 | * HOMEPAGE: 48 | * https://github.com/apple/swift-nio 49 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-currency", 8 | products: [ 9 | .library(name: "Currency", targets: ["Currency"]), 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target( 14 | name: "Currency", 15 | dependencies: [], 16 | plugins: ["ISOStandardCodegenPlugin"] 17 | ), 18 | .testTarget(name: "CurrencyTests", dependencies: ["Currency"]), 19 | 20 | .executableTarget( 21 | name: "ISOStandardCodegen" 22 | ), 23 | .plugin( 24 | name: "ISOStandardCodegenPlugin", 25 | capability: .buildTool(), 26 | dependencies: ["ISOStandardCodegen"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Package@swift-6.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-currency", 8 | products: [ 9 | .library(name: "Currency", targets: ["Currency"]), 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target( 14 | name: "Currency", 15 | dependencies: [], 16 | plugins: ["ISOStandardCodegenPlugin"] 17 | ), 18 | .testTarget(name: "CurrencyTests", dependencies: ["Currency"]), 19 | 20 | .executableTarget( 21 | name: "ISOStandardCodegen" 22 | ), 23 | .plugin( 24 | name: "ISOStandardCodegenPlugin", 25 | capability: .buildTool(), 26 | dependencies: ["ISOStandardCodegen"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Plugins/ISOStandardCodegenPlugin/plugin.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024-2025 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import PackagePlugin 16 | 17 | @main 18 | struct ISOCurrencies: BuildToolPlugin { 19 | #if swift(>=6) && compiler(>=6) 20 | func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { 21 | let dataInputURL = context.package.directoryURL.appending(path: "ISO4217.json") 22 | 23 | let currencyDefinitionURL = context.pluginWorkDirectoryURL.appending(path: "ISOCurrencies.swift") 24 | let mintDefinitionURL = context.pluginWorkDirectoryURL.appending(path: "CurrencyMint+ISOCurrencyLookup.swift") 25 | 26 | return [ 27 | .buildCommand( 28 | displayName: "Generating ISO Standard currency support code", 29 | executable: try context.tool(named: "ISOStandardCodegen").url, 30 | arguments: [ 31 | dataInputURL.path(), 32 | currencyDefinitionURL.path(), 33 | mintDefinitionURL.path() 34 | ], 35 | environment: [:], 36 | inputFiles: [ 37 | dataInputURL 38 | ], 39 | outputFiles: [ 40 | currencyDefinitionURL, 41 | mintDefinitionURL 42 | ] 43 | ) 44 | ] 45 | } 46 | #else 47 | func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { 48 | let dataInputPath = context.package.directory.appending("ISO4217.json") 49 | 50 | let currencyDefinitionPath = context.pluginWorkDirectory.appending("ISOCurrencies.swift") 51 | let mintDefinitionPath = context.pluginWorkDirectory.appending("CurrencyMint+ISOCurrencyLookup.swift") 52 | 53 | return [.buildCommand( 54 | displayName: "Generating ISO Standard currency support code", 55 | executable: try context.tool(named: "ISOStandardCodegen").path, 56 | arguments: [ 57 | dataInputPath.string, 58 | currencyDefinitionPath.string, 59 | mintDefinitionPath.string 60 | ], 61 | inputFiles: [ 62 | dataInputPath 63 | ], 64 | outputFiles: [ 65 | currencyDefinitionPath, 66 | mintDefinitionPath 67 | ] 68 | )] 69 | } 70 | #endif 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Currency 2 | 3 | [![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/peek-travel/swift-currency/blob/master/LICENSE.txt) 4 | [![Documentation Badge](https://img.shields.io/badge/Documentation-gray?style=flat&logo=gitbook) 5 | ](https://swiftpackageindex.com/peek-travel/swift-currency/main/documentation/currency) 6 | 7 | [![Swift Compatability](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpeek-travel%2Fswift-currency%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/peek-travel/swift-currency) 8 | [![Platform Compatability](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpeek-travel%2Fswift-currency%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/peek-travel/swift-currency) 9 | 10 | [![Package Tests](https://github.com/peek-travel/swift-currency/workflows/Package%20Tests/badge.svg)](https://github.com/peek-travel/swift-currency/actions?query=workflow%3A%22Package+Tests%22) 11 | [![codecov](https://codecov.io/gh/peek-travel/swift-currency/branch/main/graph/badge.svg)](https://codecov.io/gh/peek-travel/swift-currency) 12 | 13 | [![Maintainability](https://api.codeclimate.com/v1/badges/f17c8f5d598f61ee1a63/maintainability)](https://codeclimate.com/github/peek-travel/swift-currency/maintainability) 14 | 15 | ## Introduction 16 | 17 | Swift Currency provides an [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) framework for representing currencies in a type-safe way in Swift. 18 | 19 | It provides many conveniences for working with currencies, such as literal representations, value formatting, and mathematics. 20 | 21 | ```swift 22 | import Currency 23 | import Foundation 24 | 25 | let dollars = USD(30.01) 26 | print(dollars) 27 | // 30.01 USD 28 | print(dollars * 2) 29 | // 60.02 USD 30 | print(dollars.distributedEvenly(intoParts: 6)) 31 | // [USD(1.68), USD(1.68), USD(1.68), USD(1.67), USD(1.67), USD(1.67)] 32 | 33 | let pounds = GBP(109.23) 34 | print(dollars + pounds) 35 | // compile error 36 | 37 | let jpy: JPY = 399 38 | print("The total price is \(jpy.localizedString()).") 39 | // "The total price is ¥399.", assuming `Foundation.Locale.current` is "en_US" 40 | 41 | let euro = EUR(29.09) 42 | print("Der Gesamtpreis beträgt \(localize: euro, for: .init(identifier: "de_DE")).") 43 | // "Der Gesamtpreis beträgt 29,09 €." 44 | ``` 45 | 46 | > _For more detailed examples, see the [documentation](https://swiftpackageindex.com/peek-travel/swift-currency/main/documentation/currency)._ 47 | 48 | ## Language and Platform Support 49 | 50 | This library makes a best effort to run tests everywhere it is officially possible to use this library, as defined as official language support. 51 | 52 | However, the macOS cloud runners for GitHub Actions makes it a bit difficult to test the entire range of possibilities 53 | as they deprecate macOS versions or don't provide installs of newer Xcode versions. 54 | 55 | To verify if the package works as expected for your desired platform, review the latest [test results](https://github.com/peek-travel/swift-currency/actions/workflows/tests.yml). 56 | 57 | ## Installing 58 | 59 | Add the package reference to your **Package.swift** to install via SwiftPM. 60 | 61 | ```swift 62 | dependencies: [ 63 | .package(url: "https://github.com/peek-travel/swift-currency", .upToNextMajor(from: "1.0.0")) 64 | ] 65 | ``` 66 | 67 | ## Documentation 68 | 69 | Documentation is available from the [Swift Package Index](https://swiftpackageindex.com/peek-travel/swift-currency/documentation). 70 | 71 | ## Questions 72 | 73 | For bugs or feature requests, file a new [issue](https://github.com/peek-travel/swift-currency/issues). 74 | 75 | ## Changelog 76 | 77 | [SemVer](https://semver.org/) changes are documented for each release on the [releases page](https://github.com/peek-travel/swift-currency/releases). 78 | 79 | ## Contributing 80 | 81 | Check out [CONTRIBUTING.md](https://github.com/peek-travel/swift-currency/blob/master/CONTRIBUTING.md) for more information on how to help with **SwiftCurrency**. 82 | 83 | ## Contributors 84 | 85 | Check out [CONTRIBUTORS.txt](https://github.com/peek-travel/swift-currency/blob/master/CONTRIBUTORS.txt) to see the full list. This list is updated for each release. 86 | 87 | ## License 88 | 89 | [MIT](https://github.com/peek-travel/swift-currency/blob/master/LICENSE.txt) 90 | 91 | Copyright (c) 2020-present, Peek Travel Inc. 92 | 93 | _This project contains code written by others not affliated with this project. All copyright claims are reserved by them. For a full list, with their claimed rights, see [NOTICE.txt](https://github.com/peek-travel/swift-currency/blob/master/NOTICE.txt)_ 94 | 95 | _**Swift** is a registered trademark of **Apple, Inc**. Any use of their trademark does not imply any affiliation with or endorsement by them, and all rights are reserved by them._ 96 | -------------------------------------------------------------------------------- /Sources/Currency/CurrencyDescriptor.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Decimal 16 | 17 | /// A type that provides information describing a currency in terms as defined by ISO 4217. 18 | public protocol CurrencyDescriptor { 19 | /// The name of the currency, such as "United States Dollar". 20 | static var name: String { get } 21 | /// The ISO 4217 3-digit letter currency code. 22 | /// 23 | /// For example: "USD" for "United States Dollar". 24 | static var alphabeticCode: String { get } 25 | /// The ISO 4217 3-digit numeric currency code. 26 | /// 27 | /// This code is nomally the same as the ISO 3166-1 country codes, where appropriate. 28 | /// 29 | /// For example, "United States of America" has the ISO 3166-1 code of 840, which is the same for the "USD" currency in ISO 4217. 30 | static var numericCode: UInt16 { get } 31 | /// The number of decimal digits used to express minor units of the currency. 32 | /// 33 | /// For example, the US Dollar has the minor unit (cents) that are 1/100 of a dollar. Therefore, the the minorUnits is `2`. 34 | /// 35 | /// However, the Japanese Yen has no minor unit, so it has `0` minorUnits. 36 | static var minorUnits: UInt8 { get } 37 | } 38 | 39 | // MARK: Minor Units conversions 40 | 41 | @usableFromInline 42 | internal enum CurrencyMinorUnitConversionSource { 43 | case minorUnits, exactAmount 44 | } 45 | 46 | extension CurrencyDescriptor { 47 | internal static func minorUnitsCoefficient(for source: CurrencyMinorUnitConversionSource) -> Decimal { 48 | let exponent: Int 49 | switch source { 50 | case .exactAmount: exponent = .init(Self.minorUnits) 51 | case .minorUnits: exponent = -.init(Self.minorUnits) 52 | } 53 | return .init(sign: .plus, exponent: exponent, significand: 1) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Currency/CurrencyMint.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Decimal 16 | 17 | // MARK: CurrencyIdentifier 18 | 19 | extension CurrencyMint { 20 | /// An identifier of a currency. 21 | /// 22 | /// Numeric codes are normally between 0-999 in value. 23 | /// 24 | /// Alphabetic codes are normally 3 Latin characters, and will be normalized to all capital letters. 25 | public enum CurrencyIdentifier: Equatable, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral { 26 | case alphaCode(String), numericCode(UInt16) 27 | 28 | /// Creates an identifier to represent the alphabetic code. 29 | /// - Parameter value: The alphabetic code identifier. 30 | public init(_ value: String) { self = .alphaCode(value.uppercased()) } 31 | 32 | /// Creates an identifier to represent the numeric code. 33 | /// - Parameter value: The numeric code identifier. 34 | public init(_ value: UInt16) { self = .numericCode(value) } 35 | 36 | public init(stringLiteral value: String) { self.init(value) } 37 | public init(integerLiteral value: UInt16) { self.init(value) } 38 | 39 | public static func ==(lhs: CurrencyIdentifier, rhs: CurrencyIdentifier) -> Bool { 40 | switch (lhs, rhs) { 41 | case let (.alphaCode(lhsValue), .alphaCode(rhsValue)): return lhsValue == rhsValue 42 | case let (.numericCode(lhsValue), .numericCode(rhsValue)): return lhsValue == rhsValue 43 | default: return false 44 | } 45 | } 46 | } 47 | } 48 | 49 | // MARK: CurrencyMint 50 | 51 | /// A factory object that supports creation instances of concrete currency types 52 | /// by their alphabetic or numeric code identifiers. 53 | public final class CurrencyMint { 54 | /// A closure that receives a currency identifier and finds a matching concrete currency type. 55 | public typealias IdentifierLookup = (CurrencyIdentifier) -> (any CurrencyValue.Type)? 56 | 57 | /// Returns a shared currency generator that only provides ISO 4217 defined currencies. 58 | public static var standard: CurrencyMint { 59 | return .init(fallbackLookup: { _ in nil }) 60 | } 61 | 62 | private let fallbackLookup: IdentifierLookup 63 | 64 | /// Creates an instance that will use the provided lookup closure if an identifier doesn't match the ISO 4217 specification. 65 | /// - Parameter fallbackLookup: An escaping closure that will be invoked when a currency's identifier is not found in the ISO 4217 specification. 66 | public init(fallbackLookup: @escaping IdentifierLookup) { 67 | self.fallbackLookup = fallbackLookup 68 | } 69 | } 70 | 71 | // MARK: Custom Initializers 72 | 73 | extension CurrencyMint { 74 | /// Creates an instance that will always resolves the provided currency type when ISO 4217 specification lookup fails. 75 | /// - Parameter defaultCurrency: The default currency type to provide when a currency's identifier is not found in the ISO 4217 specification. 76 | public convenience init(defaultCurrency: (some CurrencyValue).Type) { 77 | self.init(fallbackLookup: { _ in defaultCurrency }) 78 | } 79 | } 80 | 81 | // MARK: Factory Methods 82 | 83 | extension CurrencyMint { 84 | /// Creates a currency value for the provided identifier. 85 | /// - Parameters: 86 | /// - identifier: The identifier of the currency to be created. 87 | /// - minorUnits: The quantity of minor units the currency value should represent. The default is `0`. 88 | /// - Returns: An instance of a currency that matches the provided identifier with the desired amount; otherwise `nil`. 89 | public func make(identifier: CurrencyIdentifier, minorUnits value: Int64 = .zero) -> (any CurrencyValue)? { 90 | guard let currencyType = self.lookup(identifier) else { return nil } 91 | return currencyType.init(minorUnits: value) 92 | } 93 | 94 | /// Creates a currency value for the provided identifier. 95 | /// - Parameters: 96 | /// - identifier: The identifier of the currency to be created. 97 | /// - value: The amount the currency value should represent. 98 | /// - Returns: An instance of a currency that matches the provided identifier with the desired amount; otherwise `nil`. 99 | public func make(identifier: CurrencyIdentifier, exactAmount value: Decimal) -> (any CurrencyValue)? { 100 | guard let currencyType = self.lookup(identifier) else { return nil } 101 | return currencyType.init(exactAmount: value) 102 | } 103 | 104 | private func lookup(_ identifier: CurrencyIdentifier) -> (any CurrencyValue.Type)? { 105 | var typeFound: (any CurrencyValue.Type)? = nil 106 | switch identifier { 107 | case let .alphaCode(value): typeFound = CurrencyMint.lookup(byAlphaCode: value) 108 | case let .numericCode(value): typeFound = CurrencyMint.lookup(byNumCode: value) 109 | } 110 | return typeFound ?? self.fallbackLookup(identifier) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/Currency/CurrencyValue+Algorithms.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | // MARK: Even Distribution 18 | 19 | extension CurrencyValue { 20 | /// Distributes the current amount into a set number of parts as evenly as possible. 21 | /// 22 | /// After splitting the amount evenly, any remainder will be given to the first value in the result collection. 23 | /// - Note: Passing a negative or `0` value will result in an empty result. 24 | /// - Complexity: O(1) 25 | /// - Parameter numParts: The count of new values the single value should be distributed between as evenly as possible. 26 | /// - Returns: A collection of currency values with their share of the amount distribution. 27 | public func distributedEvenly(intoParts numParts: Int) -> [Self] { 28 | guard numParts > 0 else { return [] } 29 | 30 | let count = CurrencyMinorUnitRepresentation(numParts) 31 | 32 | let units = self.minorUnits 33 | let fraction = units / count 34 | let remainder = Int(abs(units) % count) 35 | 36 | let remainderCollection: [Self] = .init(repeating: Self(minorUnits: fraction + units.signum()), count: remainder) 37 | let distributedCollection: [Self] = .init(repeating: Self(minorUnits: fraction), count: numParts - remainder) 38 | return remainderCollection + distributedCollection 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Currency/CurrencyValue+Arithmetic.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | // MARK: Identity 18 | 19 | extension CurrencyValue { 20 | /// The zero value. 21 | /// 22 | /// Zero is the identity element for addition. For any value, 23 | /// `x + .zero == x` and `.zero + x == x`. 24 | @inlinable 25 | public static var zero: Self { return Self(exactAmount: .zero) } 26 | 27 | /// Returns the current value as its additive inverse. 28 | /// 29 | /// The following example uses the `negated()` method to negate the currency value: 30 | /// 31 | /// let negativeAmount = USD(3.40).negated() 32 | /// // negativeAmount == USD(-3.40) 33 | /// - Complexity: O(1) 34 | @inlinable 35 | public func negated() -> Self { 36 | return Self(exactAmount: self.exactAmount * -1) 37 | } 38 | } 39 | 40 | // MARK: Addition 41 | 42 | extension CurrencyValue { 43 | // perhaps convert to minor units, multiply, then convert back to decimal? 44 | 45 | public static func +(lhs: Self, rhs: Self) -> Self { 46 | return .init(exactAmount: lhs.exactAmount + rhs.exactAmount) 47 | } 48 | public static func +=(lhs: inout Self, rhs: Self) { lhs = lhs + rhs } 49 | 50 | /// Adds the given other amount to the current exactAmount. 51 | /// - Parameter amount: The other amount to add. 52 | @inlinable 53 | public mutating func add(_ amount: Self) { 54 | self += amount 55 | } 56 | /// Adds the other amount to the current exactAmount as a new value. 57 | /// - Parameter amount: The other amount to add. 58 | public func adding(_ amount: Self) -> Self { 59 | return self + amount 60 | } 61 | 62 | public static func +(lhs: Self, rhs: some BinaryInteger) -> Self { 63 | guard let rhs = Decimal(exactly: rhs) else { 64 | return .init(exactAmount: .nan) 65 | } 66 | return .init(exactAmount: lhs.exactAmount + rhs) 67 | } 68 | public static func +=(lhs: inout Self, rhs: some BinaryInteger) { lhs = lhs + rhs } 69 | 70 | /// Adds the given other amount to the current exactAmount. 71 | /// - Parameter amount: The other amount to add. 72 | @inlinable 73 | public mutating func add(_ amount: some BinaryInteger) { 74 | self += amount 75 | } 76 | /// Adds the other amount to the current exactAmount as a new value. 77 | /// - Parameter amount: The other amount to add. 78 | public func adding(_ amount: some BinaryInteger) -> Self { 79 | return self + amount 80 | } 81 | 82 | public static func +(lhs: Self, rhs: Decimal) -> Self { 83 | return .init(exactAmount: lhs.exactAmount + rhs) 84 | } 85 | public static func +=(lhs: inout Self, rhs: Decimal) { lhs = lhs + rhs } 86 | 87 | /// Adds the given other amount to the current exactAmount. 88 | /// - Parameter amount: The other amount to add. 89 | @inlinable 90 | public mutating func add(_ amount: Decimal) { 91 | self += amount 92 | } 93 | /// Adds the other amount to the current exactAmount as a new value. 94 | /// - Parameter amount: The other amount to add. 95 | public func adding(_ amount: Decimal) -> Self { 96 | return self + amount 97 | } 98 | } 99 | 100 | // MARK: Subtraction 101 | 102 | extension CurrencyValue { 103 | // perhaps convert to minor units, multiply, then convert back to decimal? 104 | 105 | public static func -(lhs: Self, rhs: Self) -> Self { 106 | return .init(exactAmount: lhs.exactAmount - rhs.exactAmount) 107 | } 108 | public static func -=(lhs: inout Self, rhs: Self) { lhs = lhs - rhs } 109 | 110 | /// Subtracts the given other amount from the current exactAmount. 111 | /// - Parameter amount: The other amount to subtract. 112 | @inlinable 113 | public mutating func subtract(_ amount: Self) { 114 | self -= amount 115 | } 116 | /// Subtracts the other amount from the current exactAmount as a new value. 117 | /// - Parameter amount: The other amount to subtract. 118 | public func subtracting(_ amount: Self) -> Self { 119 | return self - amount 120 | } 121 | 122 | public static func -(lhs: Self, rhs: some BinaryInteger) -> Self { 123 | guard let rhs = Decimal(exactly: rhs) else { 124 | return .init(exactAmount: .nan) 125 | } 126 | return .init(exactAmount: lhs.exactAmount - rhs) 127 | } 128 | public static func -=(lhs: inout Self, rhs: some BinaryInteger) { lhs = lhs - rhs } 129 | 130 | /// Subtracts the given other amount from the current exactAmount. 131 | /// - Parameter amount: The other amount to subtract. 132 | @inlinable 133 | public mutating func subtract(_ amount: some BinaryInteger) { 134 | self -= amount 135 | } 136 | /// Subtracts the other amount from the current exactAmount as a new value. 137 | /// - Parameter amount: The other amount to subtract. 138 | public func subtracting(_ amount: some BinaryInteger) -> Self { 139 | return self - amount 140 | } 141 | 142 | public static func -(lhs: Self, rhs: Decimal) -> Self { 143 | return .init(exactAmount: lhs.exactAmount - rhs) 144 | } 145 | public static func -=(lhs: inout Self, rhs: Decimal) { lhs = lhs - rhs } 146 | 147 | /// Subtracts the given other amount from the current exactAmount. 148 | /// - Parameter amount: The other amount to subtract. 149 | @inlinable 150 | public mutating func subtract(_ amount: Decimal) { 151 | self -= amount 152 | } 153 | /// Subtracts the other amount from the current exactAmount as a new value. 154 | /// - Parameter amount: The other amount to subtract. 155 | public func subtracting(_ amount: Decimal) -> Self { 156 | return self - amount 157 | } 158 | } 159 | 160 | // MARK: Multiplication 161 | 162 | extension CurrencyValue { 163 | // perhaps convert to minor units, multiply, then convert back to decimal? 164 | // let result = Double(lhs.minorUnits * rhs.minorUnits) / pow(10, Double(Self.metadata.minorUnits)) 165 | 166 | public static func *(lhs: Self, rhs: some BinaryInteger) -> Self { 167 | guard let rhs = Decimal(exactly: rhs) else { 168 | return .init(exactAmount: .nan) 169 | } 170 | return .init(exactAmount: lhs.exactAmount * rhs) 171 | } 172 | public static func *=(lhs: inout Self, rhs: some BinaryInteger) { lhs = lhs * rhs } 173 | 174 | /// Multiplies the current exactAmount by the given other amount. 175 | /// - Parameter amount: The other amount to multiply by. 176 | @inlinable 177 | public mutating func multiply(by amount: some BinaryInteger) { 178 | self *= amount 179 | } 180 | /// Multiplies the current exactAmount by the other amount as a new value. 181 | /// - Parameter amount: The other amount to multiply by. 182 | public func multiplying(by amount: some BinaryInteger) -> Self { 183 | return self * amount 184 | } 185 | 186 | public static func *(lhs: Self, rhs: Decimal) -> Self { 187 | return .init(exactAmount: lhs.exactAmount * rhs) 188 | } 189 | public static func *=(lhs: inout Self, rhs: Decimal) { lhs = lhs * rhs } 190 | 191 | /// Multiplies the current exactAmount by the given other amount. 192 | /// - Parameter amount: The other amount to multiply by. 193 | @inlinable 194 | public mutating func multiply(by amount: Decimal) { 195 | self *= amount 196 | } 197 | /// Multiplies the current exactAmount by the other amount as a new value. 198 | /// - Parameter amount: The other amount to multiply by. 199 | public func multiplying(by amount: Decimal) -> Self { 200 | return self * amount 201 | } 202 | } 203 | 204 | // MARK: Division 205 | 206 | extension CurrencyValue { 207 | // perhaps convert to minor units, multiply, then convert back to decimal? 208 | // let quotent = Double(lhs.minorUnits) / .init(rhs.minorUnits) 209 | // let result = quotent * pow(10, Double(Self.metadata.minorUnits)) 210 | 211 | public static func /(lhs: Self, rhs: some BinaryInteger) -> Self { 212 | guard let rhs = Decimal(exactly: rhs) else { 213 | return .init(exactAmount: .nan) 214 | } 215 | return lhs / rhs 216 | } 217 | public static func /=(lhs: inout Self, rhs: some BinaryInteger) { lhs = lhs / rhs } 218 | 219 | /// Divides the current exactAmount by the given other amount. 220 | /// - Parameter amount: The other amount to divide by. 221 | @inlinable 222 | public mutating func divide(by amount: some BinaryInteger) { 223 | self /= amount 224 | } 225 | /// Divides the current exactAmount by the other amount as a new value. 226 | /// - Parameter amount: The other amount to divide by. 227 | public func dividing(by amount: some BinaryInteger) -> Self { 228 | return self / amount 229 | } 230 | 231 | public static func /(lhs: Self, rhs: Decimal) -> Self { 232 | return .init(exactAmount: lhs.exactAmount / rhs) 233 | } 234 | public static func /=(lhs: inout Self, rhs: Decimal) { lhs = lhs / rhs } 235 | 236 | /// Divides the current exactAmount by the given other amount. 237 | /// - Parameter amount: The other amount to divide by. 238 | @inlinable 239 | public mutating func divide(by amount: Decimal) { 240 | self /= amount 241 | } 242 | /// Divides the current exactAmount by the other amount as a new value. 243 | /// - Parameter amount: The other amount to divide by. 244 | public func dividing(by amount: Decimal) -> Self { 245 | return self / amount 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Sources/Currency/CurrencyValue+StringRepresentation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Locale 16 | import class Foundation.NumberFormatter 17 | import class Foundation.NSDecimalNumber 18 | 19 | // price.description 3000.98 USD 20 | // price.debugDescription USD(3000.98) 21 | // price.playgroundDescription USD(3000.98) 22 | // "\(localize: price)" $3,000.98 23 | // "\(localize: price, with: ...)" $3,000.98 24 | // "\(localize: price, for: ...)" $3,000.98 25 | 26 | // MARK: CustomStringConvertible 27 | 28 | extension CurrencyValue { 29 | public var description: String { "\(self.exactAmount) \(Self.descriptor.alphabeticCode)"} 30 | public var debugDescription: String { 31 | "\(Self.descriptor.alphabeticCode)(exact: \(self.exactAmount.description), minorUnits: \(self.minorUnits)" 32 | } 33 | public var playgroundDescription: Any { self.debugDescription } 34 | } 35 | 36 | // MARK: Localization 37 | 38 | extension String.StringInterpolation { 39 | /// Creates a string representation of the currency value, localized to a particular locale. 40 | /// 41 | /// Example 42 | /// 43 | /// let pounds = GBP(30.03) 44 | /// let localizedString = "\(localize: pounds, for: Locale(identifier: "de_DE"))" 45 | /// print(localizedString) 46 | /// // 30,03 £ 47 | /// print("\(localize: pounds)") 48 | /// // £30.03, assuming `Locale.current` is "en_US" 49 | /// 50 | /// This can also be done with a method on the value itself: 51 | /// 52 | /// let usd = USD(30.03) 53 | /// print(usd.localizedString(for: .init(identifier: "fr_FR"))) 54 | /// // 30,03 $US 55 | /// print(usd.localizedString()) 56 | /// // $30.03, assuming `Locale.current` is "en_US" 57 | /// 58 | /// - Important: This creates a new `NumberFormatter` every time it is called. 59 | /// If you have a cached instance, use `(localize:with:nilDescription:)` instead. 60 | /// - Parameters: 61 | /// - value: The value to localize. If the value is `nil`, then the `nilDescription` will be used. 62 | /// - locale: The Locale to localize the value for. The default is `.current`, ig. the runtime environment's Locale. 63 | /// - nilDescription: The optional description to use when the `value` is `nil`. 64 | #if swift(<5.8) 65 | #else 66 | @_documentation(visibility: private) 67 | #endif 68 | public mutating func appendInterpolation( 69 | localize value: Currency?, 70 | for locale: Locale = .current, 71 | nilDescription: String = "nil" 72 | ) { 73 | guard case let .some(value) = value else { return nilDescription.write(to: &self) } 74 | 75 | let formatter = NumberFormatter() 76 | formatter.numberStyle = .currency 77 | formatter.locale = locale 78 | formatter.currencyCode = Currency.descriptor.alphabeticCode 79 | 80 | self.appendInterpolation(localize: value, with: formatter, nilDescription: nilDescription) 81 | } 82 | 83 | /// Creates a string representation of the currency value, using the provided formatter. 84 | /// 85 | /// let formatter = ... 86 | /// let currency = ... 87 | /// let localizedString = "\(localize: currency, with: formatter)" 88 | /// 89 | /// This can also be done with a method on the value itself: 90 | /// 91 | /// let formatter = NumberFormatter() 92 | /// formatter.numberStyle = .currency 93 | /// formatter.currencyGroupingSeparator = " " 94 | /// formatter.currencyDecimalSeparator = "'" 95 | /// formatter.currencyCode = "GBP" 96 | /// 97 | /// let pounds = GBP(14928.02) 98 | /// print(pounds.localizedString(using: formatter)) 99 | /// // £14 928'02 100 | /// 101 | /// - Important: This method does not modify the given `Foundation.NumberFormatter`. 102 | /// If the same formatter is to be used for different currencies, 103 | /// the property will need to be updated before calling this method. 104 | /// - Parameters: 105 | /// - value: The value to localize. If the value is `nil`, then the `nilDescription` will be used. 106 | /// - formatter: The pre-configured formatter to use. 107 | /// - nilDescription: The optional description to use when the `value` is `nil`. 108 | #if swift(<5.8) 109 | #else 110 | @_documentation(visibility: private) 111 | #endif 112 | public mutating func appendInterpolation( 113 | localize value: Currency?, 114 | with formatter: NumberFormatter, 115 | nilDescription: String = "nil" 116 | ) { 117 | guard case let .some(value) = value else { return nilDescription.write(to: &self) } 118 | 119 | guard let localizedString = formatter.string(from: value.exactAmount as NSDecimalNumber) else { 120 | nilDescription.write(to: &self) 121 | return 122 | } 123 | 124 | localizedString.write(to: &self) 125 | } 126 | } 127 | 128 | extension CurrencyValue { 129 | /// Creates a string representation of the currency value, localized to a particular locale. 130 | /// 131 | /// let usd = USD(30.03) 132 | /// print(usd.localizedString(for: .init(identifier: "fr_FR"))) 133 | /// // 30,03 $US 134 | /// print(usd.localizedString()) 135 | /// // $30.03, assuming `Locale.current` is "en_US" 136 | /// 137 | /// This can also be done with String interpolation: 138 | /// 139 | /// let pounds = GBP(30.03) 140 | /// let localizedString = "\(localize: pounds, for: Locale(identifier: "de_DE"))" 141 | /// print(localizedString) 142 | /// // 30,03 £ 143 | /// print("\(localize: pounds)") 144 | /// // £30.03, assuming `Locale.current` is "en_US" 145 | /// 146 | /// - Parameters: 147 | /// - locale: The Locale to localize the value for. The default is `.current`, ig. the runtime environment's Locale. 148 | /// - Returns: A localized String representation of the currency value. 149 | @inlinable 150 | public func localizedString(for locale: Locale = .current) -> String { 151 | return "\(localize: self, for: locale)" 152 | } 153 | 154 | /// Creates a string representation of the currency value, using the provided formatter. 155 | /// 156 | /// let formatter = NumberFormatter() 157 | /// formatter.numberStyle = .currency 158 | /// formatter.currencyGroupingSeparator = " " 159 | /// formatter.currencyDecimalSeparator = "'" 160 | /// formatter.currencyCode = "GBP" 161 | /// 162 | /// let pounds = GBP(14928.02) 163 | /// print(pounds.localizedString(using: formatter)) 164 | /// // £14 928'02 165 | /// 166 | /// This can also be done with String interpolation: 167 | /// 168 | /// let formatter = ... 169 | /// let currency = ... 170 | /// let localizedString = "\(localize: currency, with: formatter)" 171 | /// 172 | /// - Important: This method does not modify the given `Foundation.NumberFormatter`. 173 | /// If the same formatter is to be used for different currencies, the property will need to be updated before calling this method. 174 | /// - Parameters: 175 | /// - formatter: The pre-configured formatter to use. 176 | /// - Returns: A localized String representation of the currency value. 177 | @inlinable 178 | public func localizedString(using formatter: NumberFormatter) -> String { 179 | return "\(localize: self, with: formatter)" 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/Currency/CurrencyValue.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Decimal 16 | import class Foundation.NSDecimalNumber 17 | import func Foundation.NSDecimalRound 18 | 19 | /// A monetary numeric value type. 20 | public protocol CurrencyValue: 21 | CustomStringConvertible, CustomDebugStringConvertible, CustomPlaygroundDisplayConvertible, 22 | CustomReflectable, 23 | Comparable, Hashable, 24 | ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, 25 | AdditiveArithmetic 26 | { 27 | /// The information describing the currency. 28 | @inlinable 29 | static var descriptor: any CurrencyDescriptor.Type { get } 30 | 31 | /// The exact amount of money being represented, 32 | /// even if the value is fractional of what the currency uses for its minor units. 33 | /// 34 | /// e.g. If the the currency is `USD` which uses 2 minor units (1/100th), the ``exactAmount`` may be `2.00389`. 35 | /// 36 | /// For an amount that is likely to be used in "every day" usage, see ``roundedAmount`` 37 | var exactAmount: Decimal { get } 38 | 39 | /// Creates a representation of the currency with the exact value as given. 40 | /// - Parameter exactAmount: The amount this instance should represent, without any rounding. 41 | init(exactAmount: Decimal) 42 | } 43 | 44 | // MARK: Defaults 45 | 46 | extension CurrencyValue where Self: CurrencyDescriptor { 47 | public static var descriptor: any CurrencyDescriptor.Type { Self.self } 48 | } 49 | 50 | // MARK: Extensions 51 | 52 | extension CurrencyValue { 53 | /// The information describing the currency. 54 | /// 55 | /// This is primarily for use when working in type-erased contexts. 56 | /// 57 | /// When possible, prefer the static property ``descriptor-40dnd`` instead. 58 | @inlinable 59 | public var descriptor: any CurrencyDescriptor.Type { Self.descriptor } 60 | 61 | /// The amount represented as a whole number of the curreny's "minor units". 62 | /// 63 | /// For example, as the USD uses 1/100 for its minor unit, 64 | /// with the value of `USD 1.01`, ``minorUnits`` will be the value `101`. 65 | /// - Important: Fractional values of the currency are truncated. 66 | public var minorUnits: CurrencyMinorUnitRepresentation { 67 | let scaledAmount = self.exactAmount * Self.descriptor.minorUnitsCoefficient(for: .exactAmount) 68 | return .init(scaledAmount.int64Value) 69 | } 70 | 71 | /// The "every day" representation of the ``exactAmount`` of money this value represents, 72 | /// rounded using the `bankers` method to the currency's ``CurrencyDescriptor/minorUnits``. 73 | /// 74 | /// For example: 75 | /// ```swift 76 | /// let usd = USD(10.007) 77 | /// let yen = JPY(100.9) 78 | /// let dinar = KWD(100.0019) 79 | /// 80 | /// print(usd, yen, dinar) 81 | /// // "USD(10.01), JPY(101), KWD(100.002) 82 | /// ``` 83 | public var roundedAmount: Decimal { self.roundedAmount(using: .bankers) } 84 | 85 | /// Creates a representation of the currency, converting the minor units representation to a decimal format. 86 | /// 87 | /// e.g. If you provide the value `100` for a currency with `2` minor units, the `exactAmount` will be `1.00`. 88 | /// - Parameter minorUnits: The minor units this value will represent. 89 | public init(minorUnits: CurrencyMinorUnitRepresentation) { 90 | let amount = Decimal(minorUnits) * Self.descriptor.minorUnitsCoefficient(for: .minorUnits) 91 | self.init(exactAmount: amount) 92 | } 93 | 94 | /// Rounds the ``exactAmount`` of money being represented using the given rounding method 95 | /// to the currency's ``CurrencyDescriptor/minorUnits``. 96 | /// - Parameter roundingMode: The desired rounding mode to use on the original ``exactAmount``. 97 | /// - Complexity: O(1) 98 | /// - Returns: The rounded amount. 99 | public func roundedAmount(using roundingMode: NSDecimalNumber.RoundingMode) -> Decimal { 100 | var sourceAmount = self.exactAmount 101 | var result = Decimal.zero 102 | NSDecimalRound(&result, &sourceAmount, .init(Self.descriptor.minorUnits), roundingMode) 103 | return result 104 | } 105 | } 106 | 107 | // MARK: Equatable 108 | 109 | extension CurrencyValue { 110 | public static func ==(lhs: Self, rhs: Other) -> Bool { 111 | guard Self.descriptor == Other.descriptor else { return false } 112 | return lhs.exactAmount == rhs.exactAmount 113 | } 114 | } 115 | 116 | // MARK: Comparable 117 | 118 | extension CurrencyValue { 119 | public static func <(lhs: Self, rhs: Self) -> Bool { 120 | return lhs.exactAmount < rhs.exactAmount 121 | } 122 | } 123 | 124 | // MARK: Hashable 125 | 126 | extension CurrencyValue { 127 | public func hash(into hasher: inout Hasher) { 128 | hasher.combine(ObjectIdentifier(Self.descriptor)) 129 | hasher.combine(self.exactAmount) 130 | } 131 | } 132 | 133 | // MARK: CustomReflectable 134 | 135 | extension CurrencyValue { 136 | public var customMirror: Mirror { 137 | return .init(self, children: [ 138 | "exactAmount": self.exactAmount, 139 | "minorUnits": self.minorUnits, 140 | "descriptor": Self.descriptor 141 | ]) 142 | } 143 | } 144 | 145 | // MARK: Literal Representations 146 | 147 | extension CurrencyValue { 148 | public init(floatLiteral value: Double) { 149 | self.init(exactAmount: Decimal(value)) 150 | } 151 | 152 | public init(integerLiteral value: Int) { 153 | self.init(exactAmount: Decimal(value)) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Articles/currency_mathematics.md: -------------------------------------------------------------------------------- 1 | # Currency Math 2 | 3 | Understanding basic and advanced monetary mathematics. 4 | 5 | ## Overview 6 | 7 | Basic arithmetic is supported with currencies, in both the normal form and `inout` (eg. `*` vs `*=`). 8 | 9 | In addition, there is the ``CurrencyValue/negated()`` method for safely reversing the sign of a currency value. 10 | 11 | When dealing with `any CurrencyValue` instances, methods are provided to use instead of direct operators. 12 | 13 | Mutating methods are directive verbs such as `add`, while non-mutating methods are imperative verbs such as `adding`. 14 | 15 | ## Rounding Behavior 16 | 17 | When accessing the ``CurrencyValue/roundedAmount`` property, the ``CurrencyValue/exactAmount`` is rounded using 18 | the [`Foundation.Decimal.RoundingMode.Bankers`](https://developer.apple.com/documentation/foundation/decimal/roundingmode/bankers) behavior. 19 | 20 | If you want explicit control of the behavior, use the ``CurrencyValue/roundedAmount(using:)`` method. 21 | 22 | ## Advanced Algorithms 23 | 24 | Much like the Swift Evolution guidelines for when something should be added to the language or Standard Library, 25 | **SwiftCurrency** will provide "advanced" algorithms where writing code for working with currencies is difficult or error-prone. 26 | 27 | For example, an app may support splitting a bill's total at a restaurant evenly between all the table's patrons. 28 | 29 | While writing an implementation is easy in theory, in practice "minor units" can be lost as remainders in plain arithmetic, known as [penny, or salami, shaving](https://en.wikipedia.org/wiki/Salami_slicing). 30 | 31 | ```swift 32 | // naïve implementation 33 | 34 | let total = USD(15.01) 35 | let numPatrons = 3 36 | let individualTotal = total.exactAmount / Decimal(numPatrons) 37 | let splitValues: [USD] = .init( 38 | repeating: .init(exactAmount: individualTotal), 39 | count: numPatrons 40 | ) 41 | 42 | print(splitValues) 43 | // [USD(5.00), USD(5.00), USD(5.00)] - $0.01 is missing! 44 | ``` 45 | 46 | To protect developers from this common mistake, the ``CurrencyValue/distributedEvenly(intoParts:)`` method is available: 47 | 48 | ```swift 49 | let total = USD(15.01) 50 | let splitValues = total.distributedEvenly(intoParts: 3) 51 | 52 | print(splitValues) 53 | // [USD(5.01), USD(5.00), USD(5.00)] - no remainder 54 | ``` 55 | 56 | ## Topics 57 | 58 | ### Basic Arithmetic 59 | 60 | See the definitions on the protocol itself. 61 | 62 | - ``CurrencyValue`` 63 | 64 | ### Sequence Arithmetic 65 | 66 | - ``Swift/Sequence/sum()`` 67 | - ``Swift/Sequence/sum(_:)`` 68 | - ``Swift/Sequence/sum(where:)`` 69 | 70 | ### Advanced Algorithms 71 | 72 | - ``CurrencyValue/distributedEvenly(intoParts:)`` 73 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Articles/custom_currencies.md: -------------------------------------------------------------------------------- 1 | # Defining Custom Currencies 2 | 3 | How to support non-ISO standard currencies. 4 | 5 | ## Overview 6 | 7 | While **SwiftCurrency** is primarily focused on the ISO 4217 standard for currencies - the library is capable of supporting custom (or non-ISO standard) currencies. 8 | 9 | > Example: Some commodities are traded in values with a higher "minor unit" precision than the backing currency. 10 | > 11 | > Looking at gasoline prices in USD, they are represented as 1/1000 of 1 Dollar, rather than 1/100. 12 | > 13 | > Using the default `USD` representation would be inappropriate. 14 | 15 | Extending **SwiftCurrency** to support additional currency types is done in two steps: 16 | 17 | 1) Define the type representing values of the currency 18 | 2) Create a custom instance of ``CurrencyMint`` 19 | 20 | ## Defining the Currency 21 | 22 | As a baseline, your type needs to conform to ``CurrencyValue``, 23 | but it can optionally also conform to ``CurrencyDescriptor`` to avoid needing to define another type. 24 | 25 | ```swift 26 | import Foundation 27 | 28 | struct USGas: CurrencyValue, CurrencyDescriptor { 29 | public static var name: String { return "US Gas" } 30 | public static var alphabeticCode: String { return "USGas" } 31 | public static var numericCode: UInt16 { return 8401 } // prefixed with the USD numericCode 32 | public static var minorUnits: UInt8 { return 3 } 33 | 34 | let exactAmount: Decimal 35 | 36 | init(exactAmount: Decimal) { self.exactAmount = exactAmount } 37 | } 38 | 39 | let chevronPrice = USGas(3.2689) 40 | print(chevronPrice.exactAmount) // 3.2689 41 | print(chevronPrice.roundedAmount) // 3.269 42 | ``` 43 | 44 | ### Custom Mint Instances 45 | 46 | > Note: Custom instances of `CurrencyMint` are only necessary if you need runtime lookup of the currency. 47 | 48 | All that is necessary for creating custom mints is to use the ``CurrencyMint/init(fallbackLookup:)`` initializer. 49 | 50 | The closure will be queried if the requested identifier doesn't match the ISO standard definitions, 51 | returning what was provided by the closure. 52 | 53 | ```swift 54 | let customMint = CurrencyMint(fallbackLookup: { identifier in 55 | guard 56 | identifier == .alphaCode(USGas.alphabeticCode) || identifier == .numericCode(USGas.numericCode) 57 | else { 58 | return nil 59 | } 60 | 61 | return USGas.self 62 | }) 63 | 64 | let chevronPrice = customMint.make(identifier: "USGas", exactAmount: 3.023) 65 | print(type(of: chevronPrice)) // (any CurrencyValue)? 66 | print(chevronPrice) // 3.023 USGas 67 | ``` 68 | 69 | ## Topics 70 | 71 | - ``CurrencyDescriptor`` 72 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Articles/displaying_currencies.md: -------------------------------------------------------------------------------- 1 | # Displaying Currencies 2 | 3 | How to translate Swift representations into Strings for displaying to users. 4 | 5 | ## Overview 6 | 7 | While type-safety is important, as well as mathematic accuracy, it is extremely common to have a need of displaying currency values to end users. 8 | 9 | Localization is not just about translation. You also need to account for 10 | languages that read from right to left or even the meaning of symbols in specific contexts. 11 | 12 | So, while ``CurrencyValue`` conforms to [`CustomStringConvertible`](https://developer.apple.com/documentation/swift/customstringconvertible), 13 | the ``CurrencyValue/description`` property is not appropriate in most cases for displaying values to end users. 14 | 15 | For displaying values to an end user that accounts for their [`Foundation.Locale`](https://developer.apple.com/documentation/foundation/locale), 16 | the ``CurrencyValue/localizedString(for:)`` and ``CurrencyValue/localizedString(using:)`` methods are available. 17 | 18 | Both of these methods are also available when doing [string interpolation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters/#String-Interpolation). 19 | 20 | > Important: The localized string will always be for the ``CurrencyValue/roundedAmount``. 21 | 22 | ### Example 23 | ```swift 24 | // assuming `Foundation.Locale.current` is "en_US" 25 | 26 | let price = EUR(2_103.95) 27 | print("Total: \(price.localizedString())") // or \(localize: price) 28 | // €2,103.95 29 | 30 | let locale = Locale(identifier: "de_DE") 31 | 32 | print("Gesamtpreis: \(price.localizedString(for: locale))") // or \(localize: price, for: locale) 33 | // Gesamtpreis: 2.103,95 € 34 | 35 | let formatter = NumberFormatter() 36 | formatter.numberStyle = .currency 37 | formatter.locale = locale 38 | formatter.currencyCode = EUR.alphabeticCode 39 | formatter.currencyGroupingSeparator = "_" 40 | formatter.currencyDecimalSeparator = "." 41 | 42 | print("Gesamtpreis: \(price.localizedString(using: formatter))") // or \(localize: price, using: formatter) 43 | // Gesamtpreis: 2_103.95 € 44 | ``` 45 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Articles/minting_currencies.md: -------------------------------------------------------------------------------- 1 | # Creating Currency Values at Runtime 2 | 3 | How to create instances of a currency at runtime. 4 | 5 | ## Overview 6 | 7 | Instances of a ``CurrencyValue`` can be created either statically by directly referencing a conforming type, (eg. `USD(3)`), 8 | or it can be dynamically created through lookup by identifier with ``CurrencyMint``. 9 | 10 | While the former is more straight forward, it's not always possible such as when getting monetary values from an API request 11 | which may just contain the currency identifier and the value itself. 12 | 13 | ## Direct Creation 14 | 15 | All ISO 4217 standard currencies have a type definition in **SwiftCurrency**, with the alphabetic identifier as the type name. 16 | 17 | ```swift 18 | let yen = JPY.zero 19 | let dollar = USD(1776) 20 | let pound = GBP(3.14) 21 | ``` 22 | 23 | ## By Identifier Lookup 24 | 25 | When the currency type can't be determined until runtime, you create instances with ``CurrencyMint``. 26 | 27 | As the vast majority of expected use of this class is with ISO 4217 standard currencies, 28 | a singleton instance is always available from the static ``CurrencyMint/standard`` property. 29 | 30 | ```swift 31 | let yen = CurrencyMint.standard.make(identifier: "JPY") // JPY(0) 32 | let pounds = CurrencyMint.standard.make(identifier: 826, exactAmount: 30.23) // GBP(30.23) 33 | let usd = CurrencyMint.standard.make(identifier: "USD", minorUnits: 300) // USD($3.00) 34 | ``` 35 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Currency.md: -------------------------------------------------------------------------------- 1 | # ``Currency`` 2 | 3 | Interact with, and calculate values of, currencies in a type-safe, ISO 4217 compliant way. 4 | 5 | ## Overview 6 | 7 | **SwiftCurrency** provides all of the necessary details to work with any currency in terms of 8 | the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) standard. 9 | 10 | It also automatically provides definitions for any currency defined in the ISO 4217 standard. 11 | 12 | For other common but not standard-defined currencies, 13 | such as "US Gas" which uses 3 decimal points, see . 14 | 15 | The core aim of the module is absolute correctness in storing, calculating, 16 | and representing currencies within their local context. 17 | 18 | No currency conversions (exchanges) are possible. 19 | 20 | ## Core Concepts 21 | 22 | Decimal-based currencies all choose a set number of "places" that the decimal point is significant for 23 | representing the currency in physical form: it's smallest unit of measurement. 24 | 25 | This number is extremely important for rounding and other algorithms. 26 | 27 | In ISO 4217 terms, this is referred to as the currency's "minor units". 28 | 29 | > Example: The value `USD(3.00)` is `300` "minor units", as USD uses the Penny as the smallest unit at 1/100th. 30 | > 31 | > However, Japenese Yen (JPY) has a "minor unit" of 0, as their smallest unit is the Yen itself. 32 | 33 | ## Standard Library Extensions 34 | 35 | Where appropriate, extensions to Standard Library types are provided for common operations for better performance and correctness of behavior. 36 | 37 | For example, `Sequence` has several overloads for calculating a sum of values: 38 | 39 | ```swift 40 | extension Sequence where Element: CurrencyValue { 41 | // essentially an alias of reduce 42 | public func sum() -> Element 43 | 44 | // In place filter & reduce 45 | public func sum(where isIncluded: (Element) throws -> Bool) rethrows -> Element 46 | 47 | // In place map & reduce 48 | public func sum(_ transform: (Element) throws -> Element) rethrows -> Element 49 | } 50 | ``` 51 | 52 | ## Language Limitations 53 | 54 | When dealing with concrete ``CurrencyValue`` types, such as `USD`, anything you can 55 | imagine with the type should work as expected. 56 | 57 | However, if you are working with contexts of `any CurrencyValue` ([existential values](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/#Protocols-as-Types)), 58 | some of the API may not be available or work as expected. 59 | 60 | For example, operators are unable to resolve which implementation is to be used due to the type-erased `Self`. 61 | 62 | In that situation, instance methods are available to still provide the same functionality. 63 | 64 | However, you are still able to pass an `any CurrencyValue` to a context expecting `some CurrencyValue`. 65 | 66 | ```swift 67 | struct Invoice { 68 | let total: Currency 69 | init(_ total: Currency) { self.total = total } 70 | } 71 | 72 | let currency = CurrencyMint.standard.make(identifier: "USD", exactAmount: 30.03) 73 | let invoice = Invoice(currency) 74 | // type(of: invoice) == Invoice 75 | 76 | print(invoice.total) 77 | // USD(30.03) 78 | ``` 79 | 80 | ## Topics 81 | 82 | - 83 | - 84 | - ``CurrencyValue`` 85 | - ``CurrencyMint`` 86 | 87 | ### Working With Values 88 | 89 | - 90 | - 91 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Symbol Extensions/CurrencyMint.md: -------------------------------------------------------------------------------- 1 | # ``Currency/CurrencyMint`` 2 | 3 | ## Overview 4 | 5 | By default, the generator only references the [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) specification 6 | of identifiers to determine which currency type to create. 7 | 8 | ```swift 9 | let usd = CurrencyMint.standard.make(identifier: "USD", exactAmount: 30.23) 10 | print(usd) 11 | // USD(30.23) 12 | ``` 13 | 14 | > Note: _Since the entire purpose of this type is creating currency values at runtime, it made sense to use the term [**mint** in the type's name, after the facilities responsible for manufacturing a currency's coins](https://en.wikipedia.org/wiki/Mint_(facility))._ 15 | 16 | ## Fallback Currencies 17 | 18 | In some cases, it's desired to just provide a single fallback currency when type lookup fails: 19 | 20 | ```swift 21 | let mint = CurrencyMint(defaultCurrency: USD.self) 22 | let value = mint.make(identifier: "_SL", exactAmount: 300) 23 | print(value) 24 | // USD(300) 25 | ``` 26 | 27 | ## Topics 28 | 29 | ### Creating a Mint 30 | 31 | - ``CurrencyMint/standard`` 32 | - ``CurrencyMint/init(defaultCurrency:)`` 33 | 34 | ### Minting Currency Values 35 | 36 | - ``CurrencyMint/CurrencyIdentifier`` 37 | - ``make(identifier:exactAmount:)`` 38 | - ``make(identifier:minorUnits:)`` 39 | 40 | ### Customizing Mint Behavior 41 | 42 | - ``CurrencyMint/IdentifierLookup`` 43 | - ``CurrencyMint/init(fallbackLookup:)`` 44 | -------------------------------------------------------------------------------- /Sources/Currency/Documentation.docc/Symbol Extensions/CurrencyValue.md: -------------------------------------------------------------------------------- 1 | # ``Currency/CurrencyValue`` 2 | 3 | ## Overview 4 | 5 | All `CurrencyValue` conformances are scalar values of [`Foundation.Decimal`](https://developer.apple.com/documentation/foundation/decimal) 6 | with an associated ``CurrencyDescriptor`` that describes the semantics when using the value. 7 | 8 | This type conforms to several Standard Library protocols for ease of use, 9 | such as `AdditiveArithmetic`, `Hashable`, `ExpressibleByIntegerLiteral`, etc. 10 | 11 | ## Minor Units 12 | 13 | The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) standard defines the term "minor units" to refer to currencies in 14 | their smallest physical form. This is essentially the number of decimal places the currency uses in practice. 15 | 16 | The associated ``CurrencyDescriptor`` provides the ISO 4217 specified "minor units" count when evaluating details 17 | such as the ``roundedAmount`` or providing the value in terms of the ``minorUnits``. 18 | 19 | > Example: The US Dollar uses 1/100 for its minor unit. 20 | > 21 | > For the value `USD(1.0)`, `minorUnits` will equal `100`. 22 | 23 | > Important: Converting values to minor units is lossy, as the ``exactAmount`` is capable of representing fractional values. 24 | 25 | ## Equating, Hashing, and Comparing Values 26 | 27 | In all cases both the ``exactAmount`` and ``descriptor`` type are taken into account. 28 | 29 | If two values have different `exactAmount` or `descriptor` types, then they are not considered equal. 30 | 31 | ```swift 32 | let usd = USD(3.14) 33 | let otherUSD = USD(5.12) 34 | let yen = JPY(300) 35 | 36 | print(usd == yen) // false 37 | print(usd == otherUSD) // false 38 | print(usd < otherUSD) // true 39 | ``` 40 | 41 | ## Creating Values with Float Literals 42 | 43 | > Warning: Swift floating-point literals are currently initialized using binary floating-point number type, 44 | > which cannot precisely express certain values. 45 | 46 | To guarantee the desired `exactAmount`, initialize a `Foundation.Decimal` from a string literal instead. 47 | 48 | ```swift 49 | let incorrectValue = USD(exactAmount: 62.12) 50 | print(incorrectValue.exactAmount) // USD(62.11999999999998976) 51 | 52 | let amount = Decimal(string: "62.12")! 53 | let correctValue = USD(exactAmount: amount) 54 | print(correctValue.exactAmount) // USD(62.12) 55 | ``` 56 | 57 | 58 | ## Topics 59 | 60 | ### Initializers 61 | 62 | - ``init(exactAmount:)`` 63 | - ``init(minorUnits:)`` 64 | 65 | ### Currency Definition 66 | 67 | - ``descriptor-40dnd`` 68 | - ``descriptor-swift.property`` 69 | 70 | ### Accessing Values 71 | 72 | - ``zero`` 73 | - ``exactAmount`` 74 | - ``minorUnits`` 75 | - ``roundedAmount`` 76 | - ``roundedAmount(using:)`` 77 | - ``negated()`` 78 | 79 | ### Displaying Values 80 | 81 | - ``localizedString(for:)`` 82 | - ``localizedString(using:)`` 83 | 84 | ### Mutating Arithmetic 85 | 86 | - ``add(_:)-7wpq3`` 87 | - ``add(_:)-9f1dj`` 88 | - ``add(_:)-3cw4x`` 89 | - ``subtract(_:)-9bwnp`` 90 | - ``subtract(_:)-6ndyb`` 91 | - ``subtract(_:)-38x86`` 92 | - ``multiply(by:)-2wpa5`` 93 | - ``multiply(by:)-3p9l0`` 94 | - ``divide(by:)-8u0um`` 95 | - ``divide(by:)-k8i6`` 96 | 97 | ### Non-mutating Arithmetic 98 | 99 | - ``adding(_:)-6gu7j`` 100 | - ``adding(_:)-1ipge`` 101 | - ``adding(_:)-7q6a2`` 102 | - ``subtracting(_:)-66neu`` 103 | - ``subtracting(_:)-w9pr`` 104 | - ``subtracting(_:)-97z2t`` 105 | - ``multiplying(by:)-7u07n`` 106 | - ``multiplying(by:)-81o21`` 107 | - ``dividing(by:)-5whpb`` 108 | - ``dividing(by:)-69hs6`` 109 | -------------------------------------------------------------------------------- /Sources/Currency/Extensions/Decimal.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | extension Decimal { 18 | internal var int64Value: Int64 { return (self as NSNumber).int64Value } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Currency/Extensions/Sequence+Currency.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | // MARK: Sum 16 | 17 | extension Sequence where Element: CurrencyValue { 18 | /// Returns the sum total of all amounts in the sequence. 19 | /// 20 | /// For example: 21 | /// 22 | /// let amounts: [USD] = [304.98, 19.02] 23 | /// let sumTotal = amounts.sum() 24 | /// print(sumTotal) 25 | /// // prints "324" 26 | /// 27 | /// If the sequence has no elements, you will receive a currency with a value of "0". 28 | /// - Complexity: O(*n*) , where *n* is the length of the sequence. 29 | /// - Returns: A currency value representing the sum total of all the amounts in the sequence. 30 | public func sum() -> Element { 31 | return self.reduce(into: .zero, +=) 32 | } 33 | 34 | /// Returns the sum total of all amounts in the sequence that satify the given predicate. 35 | /// For example: 36 | /// 37 | /// let amounts: [USD] = [304.98, 19.02, 30.21] 38 | /// let sumTotal = amounts.sum(where: { $0.amount > 20 }) 39 | /// print(sumTotal) 40 | /// // prints "335.19" 41 | /// 42 | /// - Complexity: O(*n*), where *n* is the length of the sequence. 43 | /// - Parameter isIncluded: A closure that takes a currency element as its argument 44 | /// and returns a Boolean value that indicates whether the passed element should be included in the sum. 45 | /// - Returns: A currency value representing the sum total of all the amounts `isIncluded` allowed. 46 | @inlinable 47 | public func sum(where isIncluded: (Element) throws -> Bool) rethrows -> Element { 48 | return try self.reduce(into: .zero) { result, next in 49 | guard try isIncluded(next) else { return } 50 | result += next 51 | } 52 | } 53 | 54 | /// Returns the sum total of amounts in the sequence after applying the provided transform. 55 | /// 56 | /// Rather than doing a `.map(_:)` and then `.sum()`, the `sum` result will be calculated inline while applying the transformations. 57 | /// 58 | /// For example, you may want to calculate what the total taxes would be from a sequence of individual prices: 59 | /// 60 | /// let prices: [USD] = [3.00, 2.99, 5.98] 61 | /// // apply a 9% tax rate to each price and calculate the sum 62 | /// let totalTaxes = prices.sum { $0 * Decimal(0.09) } 63 | /// // totalTaxes == USD(1.08) 64 | /// 65 | /// - Complexity: O(*n*), where *n* is the length of the sequence. 66 | /// - Parameter transform: A mapping closure. `transform` accepts an element of this sequence as its parameter 67 | /// and returns a transformed value of the same type. 68 | /// - Returns: A currency value representing the sum total of all the transformed amounts in the sequence. 69 | @inlinable 70 | public func sum(_ transform: (Element) throws -> (Element)) rethrows -> Element { 71 | return try self.reduce(into: .zero) { $0 += try transform($1) } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Currency/MinorUnitRepresentation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | /// The integer used to represent a currency in it's 'minor units' form. 16 | /// 17 | /// e.g. 100 USD will be represented as `100`. 18 | #if swift(<5.8) 19 | #else 20 | @_documentation(visibility: private) 21 | #endif 22 | public typealias CurrencyMinorUnitRepresentation = Int64 23 | -------------------------------------------------------------------------------- /Sources/ISOStandardCodegen/Codegen+CurrencyDefinitions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | func makeISOCurrencyDefinitionFile(at destinationURL: URL, from source: [CurrencyDefinition]) throws { 18 | let typeDefinitions = makeTypeDefinitions(from: source) 19 | .joined(separator: "\n\n") 20 | 21 | let fileContent = """ 22 | \(makeFileHeader()) 23 | 24 | import struct Foundation.Decimal 25 | 26 | \(typeDefinitions) 27 | """ 28 | 29 | try fileContent.write(to: destinationURL, atomically: true, encoding: .utf8) 30 | } 31 | 32 | private func makeTypeDefinitions(from definitions: [CurrencyDefinition]) -> [String] { 33 | return definitions.map { 34 | definition in 35 | 36 | let summary: String = { 37 | let alphaIdentifier = definition.identifiers.alphabetic 38 | 39 | switch alphaIdentifier { 40 | case "XXX": 41 | return """ 42 | /// The currency to represent a transaction where no currency was involved; as defined by ISO 4217. 43 | /// 44 | /// As ISO 4217 does not specify a minor unit for XXX, the value of `0` is used. 45 | """ 46 | 47 | case "XTS": 48 | return """ 49 | /// The currency suitable for testing; as defined by ISO 4217. 50 | /// 51 | /// As ISO 4217 does not specify a minor unit for XTS, the value of `1` is used for validating rounding errors. 52 | """ 53 | 54 | default: 55 | return "/// The \(definition.name) (\(alphaIdentifier)) currency, as defined by ISO 4217." 56 | } 57 | }() 58 | 59 | let documentationFlag: String = { 60 | #if swift(<5.8) 61 | return "" 62 | #else 63 | return "@_documentation(visibility: private)" 64 | #endif 65 | }() 66 | 67 | return """ 68 | \(summary) 69 | \(documentationFlag) 70 | public struct \(definition.identifiers.alphabetic): CurrencyValue, CurrencyDescriptor { 71 | public static var name: String { "\(definition.name)" } 72 | public static var alphabeticCode: String { "\(definition.identifiers.alphabetic)" } 73 | public static var numericCode: UInt16 { \(definition.identifiers.numeric) } 74 | public static var minorUnits: UInt8 { \(definition.minorUnits) } 75 | 76 | public let exactAmount: Decimal 77 | 78 | public init(exactAmount: Decimal) { self.exactAmount = exactAmount } 79 | } 80 | """ 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ISOStandardCodegen/Codegen+DefinitionParsing.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | func parseDefinitions(at file: URL) throws -> [CurrencyDefinition] { 18 | let jsonData = try Data(contentsOf: file) 19 | 20 | return try JSONDecoder().decode([CurrencyDefinition].self, from: jsonData) 21 | } 22 | 23 | struct CurrencyDefinition { 24 | let name: String 25 | let identifiers: (alphabetic: String, numeric: Int) 26 | let minorUnits: Int 27 | } 28 | 29 | extension CurrencyDefinition: Decodable { 30 | enum CodingKeys: String, CodingKey { 31 | case name = "CcyNm" 32 | case alphaCode = "Ccy" 33 | case numericCode = "CcyNbr" 34 | case minorUnits = "CcyMnrUnts" 35 | } 36 | 37 | init(from decoder: any Decoder) throws { 38 | let container = try decoder.container(keyedBy: CodingKeys.self) 39 | 40 | self.name = try container.decode(String.self, forKey: .name) 41 | self.identifiers = ( 42 | try container.decode(String.self, forKey: .alphaCode), 43 | try container.decode(Int.self, forKey: .numericCode) 44 | ) 45 | self.minorUnits = try container.decode(Int.self, forKey: .minorUnits) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ISOStandardCodegen/Codegen+FileHeader.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Calendar 16 | import struct Foundation.Date 17 | 18 | func makeFileHeader() -> String { 19 | let currentYear = Calendar(identifier: .gregorian).component(.year, from: Date()) 20 | 21 | return """ 22 | //===----------------------------------------------------------------------===// 23 | // 24 | // This source file is part of the SwiftCurrency open source project 25 | // 26 | // Copyright (c) 2020-\(currentYear) SwiftCurrency project authors 27 | // Licensed under MIT License 28 | // 29 | // See LICENSE.txt for license information 30 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 31 | // 32 | // SPDX-License-Identifier: MIT 33 | // 34 | //===----------------------------------------------------------------------===// 35 | 36 | /* 37 | WARNING: 38 | 39 | This file's contents are automatically generated as part of the module's build process. 40 | 41 | Changes manually made will be lost! 42 | */ 43 | """ 44 | } 45 | -------------------------------------------------------------------------------- /Sources/ISOStandardCodegen/Codegen+MintLookup.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | func makeMintISOCurrencySupportCodeFile(at destinationURL: URL, from currencies: [CurrencyDefinition]) throws { 18 | let alphaLookupSnippet = makeIdentifierLookupSnippet(for: currencies, identifier: .alphabetic) 19 | let numericLookupSnippet = makeIdentifierLookupSnippet(for: currencies, identifier: .numeric) 20 | 21 | let fileContent = """ 22 | \(makeFileHeader()) 23 | 24 | extension CurrencyMint { 25 | \t\(alphaLookupSnippet) 26 | 27 | \t\(numericLookupSnippet) 28 | } 29 | """ 30 | 31 | try fileContent.write(to: destinationURL, atomically: true, encoding: .utf8) 32 | } 33 | 34 | enum Identifier { 35 | case alphabetic, numeric 36 | } 37 | 38 | fileprivate func makeIdentifierLookupSnippet( 39 | for currencies: [CurrencyDefinition], 40 | identifier type: Identifier 41 | ) -> String { 42 | let casesSnippet = currencies 43 | .lazy 44 | .sorted(by: type) 45 | .map { 46 | let patternMatch = type == .numeric ? $0.identifiers.numeric.description : "\"\($0.identifiers.alphabetic)\"" 47 | 48 | return "case \(patternMatch): return \($0.identifiers.alphabetic).self" 49 | } 50 | .joined(separator: "\n\t\t") 51 | 52 | let argumentName = type == .numeric ? "byNumCode" : "byAlphaCode" 53 | let parameterType = type == .numeric ? "UInt16" : "String" 54 | 55 | return """ 56 | internal static func lookup(\(argumentName) code: \(parameterType)) -> (any CurrencyValue.Type)? { 57 | \t\tswitch code { 58 | \t\t\(casesSnippet) 59 | 60 | \t\tdefault: return nil 61 | \t\t} 62 | \t} 63 | """ 64 | } 65 | 66 | extension LazySequence where Element == CurrencyDefinition { 67 | fileprivate func sorted(by identifier: Identifier) -> [Element] { 68 | self.sorted(by: { 69 | switch identifier { 70 | case .numeric: return $0.identifiers.numeric < $1.identifiers.numeric 71 | case .alphabetic: return $0.identifiers.alphabetic < $1.identifiers.alphabetic 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/ISOStandardCodegen/main.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | let arguments = ProcessInfo.processInfo.arguments 18 | guard arguments.count == 4 else { 19 | exit(65) 20 | } 21 | 22 | let isoStandardDefinitions = try parseDefinitions(at: URL(fileURLWithPath: arguments[1])) 23 | 24 | try makeISOCurrencyDefinitionFile(at: URL(fileURLWithPath: arguments[2]), from: isoStandardDefinitions) 25 | try makeMintISOCurrencySupportCodeFile(at: URL(fileURLWithPath: arguments[3]), from: isoStandardDefinitions) 26 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/CurrencyDescriptorTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @testable import Currency 16 | import XCTest 17 | 18 | final class CurrencyDescriptorTests: XCTestCase { } 19 | 20 | // MARK: Minor Units 21 | 22 | extension CurrencyDescriptorTests { 23 | func test_minorUnitsCoefficient_forExactAmount_shiftsLeftByMinorUnits() { 24 | XCTAssertEqual(JPY.minorUnitsCoefficient(for: .exactAmount), 1) 25 | XCTAssertEqual(XTS.minorUnitsCoefficient(for: .exactAmount), 10) 26 | XCTAssertEqual(USD.minorUnitsCoefficient(for: .exactAmount), 100) 27 | XCTAssertEqual(KWD.minorUnitsCoefficient(for: .exactAmount), 1000) 28 | } 29 | 30 | func test_minorUnitsCoefficient_forMinorUnits_shiftsRightByMinorUnits() { 31 | XCTAssertEqual(JPY.minorUnitsCoefficient(for: .minorUnits), 1) 32 | XCTAssertEqual(XTS.minorUnitsCoefficient(for: .minorUnits), 0.1) 33 | XCTAssertEqual(USD.minorUnitsCoefficient(for: .minorUnits), 0.01) 34 | XCTAssertEqual(KWD.minorUnitsCoefficient(for: .minorUnits), 0.001) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/CurrencyMintTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2020-2025 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Currency 16 | import XCTest 17 | 18 | final class CurrencyMintTests: XCTestCase { } 19 | 20 | // MARK: Fallback Lookup 21 | 22 | extension CurrencyMintTests { 23 | func test_defaultLookup_whenLookupFails_returnsNil() { 24 | let mint = CurrencyMint.standard 25 | 26 | var darseks = mint.make(identifier: .fakeCurrencyAlphaCode, exactAmount: 30.23) 27 | XCTAssertNil(darseks) 28 | 29 | darseks = CurrencyMint.standard.make(identifier: .fakeCurrencyNumericCode, minorUnits: 300) 30 | XCTAssertNil(darseks) 31 | } 32 | 33 | func test_withFallbackLookup_whenLookupFails_callsFallbackLookup() async { 34 | struct KED: CurrencyValue, CurrencyDescriptor { 35 | static var name: String { return "Klingon Darseks" } 36 | static var alphabeticCode: String { return "KED" } 37 | static var numericCode: UInt16 { return 666 } 38 | static var minorUnits: UInt8 { return 3 } 39 | 40 | let exactAmount: Decimal 41 | } 42 | 43 | let expectation = self.expectation(description: "fallback lookup closure to be called") 44 | expectation.expectedFulfillmentCount = 2 45 | 46 | let mint = CurrencyMint(fallbackLookup: { identifier in 47 | expectation.fulfill() 48 | 49 | guard identifier == .alphaCode("KED") || identifier == .numericCode(666) else { return nil } 50 | return KED.self 51 | }) 52 | 53 | let expectedAmount = Decimal(string: "30.239")! 54 | let d1 = mint.make(identifier: "KED", exactAmount: expectedAmount) 55 | XCTAssertTrue(d1 is KED) 56 | 57 | let d2 = mint.make(identifier: 666, minorUnits: .zero) 58 | XCTAssertTrue(d2 is KED) 59 | 60 | #if swift(>=5.9) 61 | await self.fulfillment(of: [expectation], timeout: 1) 62 | #else 63 | DispatchQueue.main.sync { 64 | self.waitForExpectations(timeout: 1) 65 | } 66 | #endif 67 | } 68 | 69 | func test_withDefaultCurrencyLookup_whenLookupFails_returnsDefaultCurrency() { 70 | let mint = CurrencyMint(defaultCurrency: USD.self) 71 | 72 | var money = mint.make(identifier: .fakeCurrencyAlphaCode) 73 | XCTAssertTrue(money is USD) 74 | 75 | money = mint.make(identifier: .fakeCurrencyNumericCode) 76 | XCTAssertTrue(money is USD) 77 | } 78 | } 79 | 80 | // MARK: Factory Methods 81 | 82 | extension CurrencyMintTests { 83 | func test_make_withMinorUnits_returnsISOCurrency() { 84 | let mint = CurrencyMint.standard 85 | 86 | let pounds = mint.make(identifier: .poundsAlphaCode, minorUnits: .zero) 87 | XCTAssertTrue(pounds is GBP) 88 | 89 | let yen = mint.make(identifier: .yenNumericCode, minorUnits: 300) 90 | XCTAssertTrue(yen is JPY) 91 | 92 | let euros = CurrencyMint.standard.make(identifier: 978, minorUnits: .zero) 93 | XCTAssertTrue(euros is EUR) 94 | } 95 | 96 | func test_make_withAmount_returnsISOCurrency() { 97 | let mint = CurrencyMint.standard 98 | 99 | let pounds = mint.make(identifier: .poundsAlphaCode, exactAmount: .zero) 100 | XCTAssertTrue(pounds is GBP) 101 | 102 | let yen = mint.make(identifier: .yenNumericCode, exactAmount: 192.8) 103 | XCTAssertTrue(yen is JPY) 104 | 105 | let euros = CurrencyMint.standard.make(identifier: 978, exactAmount: .zero) 106 | XCTAssertTrue(euros is EUR) 107 | } 108 | 109 | func test_make_withMinorUnits_matchesMinorUnits() { 110 | let mint = CurrencyMint.standard 111 | 112 | let pounds = mint.make(identifier: .poundsAlphaCode, minorUnits: 549) 113 | XCTAssertEqual(pounds?.minorUnits, 549) 114 | 115 | let yen = mint.make(identifier: .yenNumericCode, minorUnits: 302) 116 | XCTAssertEqual(yen?.minorUnits, 302) 117 | } 118 | 119 | func test_make_withAmount_matchesExactAmount() { 120 | let mint = CurrencyMint.standard 121 | 122 | let pounds = mint.make(identifier: .poundsAlphaCode, exactAmount: .zero) 123 | XCTAssertEqual(pounds?.exactAmount, .zero) 124 | 125 | let yen = mint.make(identifier: .yenNumericCode, exactAmount: 302.98) 126 | XCTAssertEqual(yen?.exactAmount, 302.98) 127 | } 128 | } 129 | 130 | // MARK: Test Helpers 131 | 132 | extension CurrencyMint.CurrencyIdentifier { 133 | fileprivate static var fakeCurrencyAlphaCode: Self { "KED" } // "darseks" 134 | fileprivate static var fakeCurrencyNumericCode: Self { 666 } 135 | 136 | fileprivate static var poundsAlphaCode: Self { .alphaCode(GBP.alphabeticCode) } 137 | fileprivate static var yenNumericCode: Self { .numericCode(JPY.numericCode) } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/CurrencyValue+AlgorithmsTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024-2025 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Currency 16 | import XCTest 17 | 18 | final class CurrencyValueAlgorithmsTests: XCTestCase { } 19 | 20 | // MARK: Distributed Evenly 21 | 22 | extension CurrencyValueAlgorithmsTests { 23 | func test_distributedEvenly_with0_orNegative_NumParts_returnsEmptyResult() { 24 | let value = USD(5) 25 | XCTAssertTrue(value.distributedEvenly(intoParts: 0).isEmpty) 26 | XCTAssertTrue(value.distributedEvenly(intoParts: -1).isEmpty) 27 | } 28 | 29 | func test_distributedEvenly_with0MinorUnits() { 30 | self.run_distributedEvenlyTest( 31 | for: JPY(10.05), 32 | numParts: 6, 33 | expectedResults: .init(repeating: JPY(2), count: 4) + .init(repeating: JPY(1), count: 2) 34 | ) 35 | } 36 | 37 | func test_distributedEvenly_with1MinorUnit() { 38 | self.run_distributedEvenlyTest( 39 | for: XTS(10.05), 40 | numParts: 6, 41 | expectedResults: .init(repeating: XTS(1.7), count: 4) + .init(repeating: XTS(1.6), count: 2) 42 | ) 43 | } 44 | 45 | func test_distributedEvenly_with2MinorUnit() { 46 | self.run_distributedEvenlyTest( 47 | for: USD(15.01), 48 | numParts: 3, 49 | expectedResults: [USD(exactAmount: Decimal(string: "5.01")!), 5, 5] 50 | ) 51 | self.run_distributedEvenlyTest( 52 | for: USD(10.05), 53 | numParts: 6, 54 | expectedResults: .init(repeating: USD(1.68), count: 3) + .init(repeating: USD(1.67), count: 3) 55 | ) 56 | } 57 | 58 | func test_distributedEvenly_with3MinorUnit() { 59 | self.run_distributedEvenlyTest( 60 | for: KWD(10.05), 61 | numParts: 6, 62 | expectedResults: .init(repeating: KWD(1.675), count: 6) 63 | ) 64 | } 65 | 66 | private func run_distributedEvenlyTest( 67 | for currency: Currency, 68 | numParts count: Int, 69 | expectedResults: [Currency], 70 | file: StaticString = #filePath, 71 | line: UInt = #line 72 | ) { 73 | guard count == expectedResults.count else { 74 | return XCTFail( 75 | "Inconsistent desire: Asked for \(count) parts, but expect \(expectedResults.count) results", 76 | file: file, line: line 77 | ) 78 | } 79 | let actualResults = currency.distributedEvenly(intoParts: count) 80 | XCTAssertEqual(actualResults, expectedResults, file: file, line: line) 81 | XCTAssertEqual(currency.roundedAmount, expectedResults.sum().roundedAmount, file: file, line: line) 82 | XCTAssertEqual( 83 | currency.negated().distributedEvenly(intoParts: count), 84 | expectedResults.map({ $0.negated() }), 85 | file: file, line: line 86 | ) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/CurrencyValue+ArithmeticTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024-2025 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Currency 16 | import XCTest 17 | 18 | final class CurrencyValueArithmeticTests: XCTestCase { } 19 | 20 | // MARK: Identity 21 | 22 | extension CurrencyValueArithmeticTests { 23 | func test_zero_equalsDecimalZero() { 24 | XCTAssertTrue(USD.zero.exactAmount == .zero) 25 | XCTAssertTrue(JPY.zero.exactAmount == .zero) 26 | XCTAssertTrue(KWD.zero.exactAmount == .zero) 27 | } 28 | 29 | func test_negated_correctlyInvertsValues() { 30 | func assertNegationIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 31 | let positive = currencyType.init(exactAmount: 30.03) 32 | XCTAssertEqual( 33 | positive.negated(), 34 | currencyType.init(exactAmount: -30.03), 35 | "positive to negative failed", 36 | file: file, line: line 37 | ) 38 | 39 | let negative = currencyType.init(exactAmount: -41.21) 40 | XCTAssertEqual( 41 | negative.negated(), 42 | currencyType.init(exactAmount: 41.21), 43 | "negative to positive failed", 44 | file: file, line: line 45 | ) 46 | } 47 | 48 | assertNegationIsCorrect(for: USD.self) 49 | assertNegationIsCorrect(for: JPY.self) 50 | assertNegationIsCorrect(for: KWD.self) 51 | } 52 | } 53 | 54 | // MARK: Addition 55 | 56 | extension CurrencyValueArithmeticTests { 57 | func test_addition_withSelf_isCorrect() { 58 | func assertAdditionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 59 | let first = currencyType.init(exactAmount: 300.12) 60 | XCTAssertEqual( 61 | first + currencyType.init(exactAmount: 30.01), 62 | currencyType.init(exactAmount: 330.13), 63 | "operator failed", 64 | file: file, line: line 65 | ) 66 | 67 | let second = currencyType.init(exactAmount: 32.12) 68 | XCTAssertEqual( 69 | second.adding(.init(exactAmount: 45)), 70 | currencyType.init(exactAmount: 77.12), 71 | "adding(_:) failed", 72 | file: file, line: line 73 | ) 74 | 75 | var third = second 76 | third.add(.init(exactAmount: 30)) 77 | XCTAssertEqual( 78 | third, 79 | currencyType.init(exactAmount: Decimal(string: "62.12")!), 80 | "add(_:) failed", 81 | file: file, line: line 82 | ) 83 | } 84 | 85 | assertAdditionIsCorrect(for: USD.self) 86 | assertAdditionIsCorrect(for: JPY.self) 87 | assertAdditionIsCorrect(for: KWD.self) 88 | } 89 | 90 | func test_addition_withBinaryInteger_isCorrect() { 91 | func assertAdditionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 92 | let first = currencyType.init(exactAmount: 300.12) 93 | XCTAssertEqual( 94 | first + (30 as Int), 95 | currencyType.init(exactAmount: 330.12), 96 | "operator failed", 97 | file: file, line: line 98 | ) 99 | 100 | let second = currencyType.init(exactAmount: 32.12) 101 | XCTAssertEqual( 102 | second.adding(45 as Int), 103 | currencyType.init(exactAmount: 77.12), 104 | "adding(_:) failed", 105 | file: file, line: line 106 | ) 107 | 108 | var third = second 109 | third.add(30 as Int) 110 | XCTAssertEqual( 111 | third, 112 | currencyType.init(exactAmount: Decimal(string: "62.12")!), 113 | "add(_:) failed", 114 | file: file, line: line 115 | ) 116 | } 117 | 118 | assertAdditionIsCorrect(for: USD.self) 119 | assertAdditionIsCorrect(for: JPY.self) 120 | assertAdditionIsCorrect(for: KWD.self) 121 | } 122 | 123 | func test_addition_withDecimal_isCorrect() { 124 | func assertAdditionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 125 | let first = currencyType.init(exactAmount: 300.12) 126 | XCTAssertEqual( 127 | first + (30 as Decimal), 128 | currencyType.init(exactAmount: 330.12), 129 | "operator failed", 130 | file: file, line: line 131 | ) 132 | 133 | let second = currencyType.init(exactAmount: 32.12) 134 | XCTAssertEqual( 135 | second.adding(45 as Decimal), 136 | currencyType.init(exactAmount: 77.12), 137 | "adding(_:) failed", 138 | file: file, line: line 139 | ) 140 | 141 | var third = second 142 | third.add(30.01 as Decimal) 143 | XCTAssertEqual( 144 | third, 145 | currencyType.init(exactAmount: Decimal(string: "62.13")!), 146 | "add(_:) failed", 147 | file: file, line: line 148 | ) 149 | } 150 | 151 | assertAdditionIsCorrect(for: USD.self) 152 | assertAdditionIsCorrect(for: JPY.self) 153 | assertAdditionIsCorrect(for: KWD.self) 154 | } 155 | } 156 | 157 | // MARK: Subtraction 158 | 159 | extension CurrencyValueArithmeticTests { 160 | func test_subtraction_withSelf_isCorrect() { 161 | func assertSubtractionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 162 | let first = currencyType.init(exactAmount: 300.12) 163 | XCTAssertEqual( 164 | first - currencyType.init(exactAmount: 30.01), 165 | currencyType.init(exactAmount: Decimal(string: "270.11")!), 166 | "operator failed", 167 | file: file, line: line 168 | ) 169 | 170 | let second = currencyType.init(exactAmount: 32.12) 171 | XCTAssertEqual( 172 | second.subtracting(.init(exactAmount: 45)), 173 | currencyType.init(exactAmount: -12.88), 174 | "subtracting(_:) failed", 175 | file: file, line: line 176 | ) 177 | 178 | var third = second 179 | third.subtract(.init(exactAmount: 30)) 180 | XCTAssertEqual( 181 | third, 182 | currencyType.init(exactAmount: Decimal(string: "2.12")!), 183 | "subtract(_:) failed", 184 | file: file, line: line 185 | ) 186 | } 187 | 188 | assertSubtractionIsCorrect(for: USD.self) 189 | assertSubtractionIsCorrect(for: JPY.self) 190 | assertSubtractionIsCorrect(for: KWD.self) 191 | } 192 | 193 | func test_subtraction_withBinaryInteger_isCorrect() { 194 | func assertSubtractionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 195 | let first = currencyType.init(exactAmount: 300.12) 196 | XCTAssertEqual( 197 | first - (30 as Int), 198 | currencyType.init(exactAmount: 270.12), 199 | "operator failed", 200 | file: file, line: line 201 | ) 202 | 203 | let second = currencyType.init(exactAmount: 32.12) 204 | XCTAssertEqual( 205 | second.subtracting(45 as Int), 206 | currencyType.init(exactAmount: -12.88), 207 | "adding(_:) failed", 208 | file: file, line: line 209 | ) 210 | 211 | var third = second 212 | third.subtract(30 as Int) 213 | XCTAssertEqual( 214 | third, 215 | currencyType.init(exactAmount: Decimal(string: "2.12")!), 216 | "add(_:) failed", 217 | file: file, line: line 218 | ) 219 | } 220 | 221 | assertSubtractionIsCorrect(for: USD.self) 222 | assertSubtractionIsCorrect(for: JPY.self) 223 | assertSubtractionIsCorrect(for: KWD.self) 224 | } 225 | 226 | func test_subtraction_withDecimal_isCorrect() { 227 | func assertSubtractionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 228 | let first = currencyType.init(exactAmount: 300.12) 229 | XCTAssertEqual( 230 | first - (30 as Decimal), 231 | currencyType.init(exactAmount: 270.12), 232 | "operator failed", 233 | file: file, line: line 234 | ) 235 | 236 | let second = currencyType.init(exactAmount: 32.12) 237 | XCTAssertEqual( 238 | second.subtracting(45 as Decimal), 239 | currencyType.init(exactAmount: -12.88), 240 | "adding(_:) failed", 241 | file: file, line: line 242 | ) 243 | 244 | var third = second 245 | third.subtract(30.01 as Decimal) 246 | XCTAssertEqual( 247 | third, 248 | currencyType.init(exactAmount: Decimal(string: "2.11")!), 249 | "add(_:) failed", 250 | file: file, line: line 251 | ) 252 | } 253 | 254 | assertSubtractionIsCorrect(for: USD.self) 255 | assertSubtractionIsCorrect(for: JPY.self) 256 | assertSubtractionIsCorrect(for: KWD.self) 257 | } 258 | } 259 | 260 | // MARK: Multiplication 261 | 262 | extension CurrencyValueArithmeticTests { 263 | func test_multiplication_withBinaryInteger_isCorrect() { 264 | func assertMultiplicationIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 265 | let first = currencyType.init(exactAmount: 300.12) 266 | XCTAssertEqual( 267 | first * (3 as Int), 268 | currencyType.init(exactAmount: 900.36), 269 | "operator failed", 270 | file: file, line: line 271 | ) 272 | 273 | let second = currencyType.init(exactAmount: 32.12) 274 | XCTAssertEqual( 275 | second.multiplying(by: -3 as Int), 276 | currencyType.init(exactAmount: -96.36), 277 | "multiplying(by:) failed", 278 | file: file, line: line 279 | ) 280 | 281 | var third = second 282 | third.multiply(by: 10 as Int) 283 | XCTAssertEqual( 284 | third, 285 | currencyType.init(exactAmount: Decimal(string: "321.2")!), 286 | "multiply(by:) failed", 287 | file: file, line: line 288 | ) 289 | } 290 | 291 | assertMultiplicationIsCorrect(for: USD.self) 292 | assertMultiplicationIsCorrect(for: JPY.self) 293 | assertMultiplicationIsCorrect(for: KWD.self) 294 | } 295 | 296 | func test_multiplication_withDecimal_isCorrect() { 297 | func assertMultiplicationIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 298 | let first = currencyType.init(exactAmount: 300.12) 299 | XCTAssertEqual( 300 | first * (5.25 as Decimal), 301 | currencyType.init(exactAmount: 1575.63), 302 | "operator failed", 303 | file: file, line: line 304 | ) 305 | 306 | let second = currencyType.init(exactAmount: 32.12) 307 | XCTAssertEqual( 308 | second.multiplying(by: -1.75 as Decimal), 309 | currencyType.init(exactAmount: -56.21), 310 | "multiplying(by:) failed", 311 | file: file, line: line 312 | ) 313 | 314 | var third = second 315 | third.multiply(by: 7.1 as Decimal) 316 | XCTAssertEqual( 317 | third, 318 | currencyType.init(exactAmount: 228.052), 319 | "multiply(by:) failed", 320 | file: file, line: line 321 | ) 322 | } 323 | 324 | assertMultiplicationIsCorrect(for: USD.self) 325 | assertMultiplicationIsCorrect(for: JPY.self) 326 | assertMultiplicationIsCorrect(for: KWD.self) 327 | } 328 | } 329 | 330 | // MARK: Division 331 | 332 | extension CurrencyValueArithmeticTests { 333 | func test_division_withBinaryInteger_isCorrect() { 334 | func assertDivisionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 335 | let first = currencyType.init(exactAmount: 300.12) 336 | XCTAssertEqual( 337 | first / (3 as Int), 338 | currencyType.init(exactAmount: 100.04), 339 | "operator failed", 340 | file: file, line: line 341 | ) 342 | 343 | let second = currencyType.init(exactAmount: 33.12) 344 | XCTAssertEqual( 345 | second.dividing(by: -3 as Int), 346 | currencyType.init(exactAmount: Decimal(string: "-11.04")!), 347 | "dividing(by:) failed", 348 | file: file, line: line 349 | ) 350 | 351 | var third = second 352 | third.divide(by: 10 as Int) 353 | XCTAssertEqual( 354 | third, 355 | currencyType.init(exactAmount: Decimal(string: "3.312")!), 356 | "divide(by:) failed", 357 | file: file, line: line 358 | ) 359 | } 360 | 361 | assertDivisionIsCorrect(for: USD.self) 362 | assertDivisionIsCorrect(for: JPY.self) 363 | assertDivisionIsCorrect(for: KWD.self) 364 | } 365 | 366 | func test_division_withDecimal_isCorrect() { 367 | func assertDivisionIsCorrect(for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 368 | let first = currencyType.init(exactAmount: 300) 369 | XCTAssertEqual( 370 | first / (2.4 as Decimal), 371 | currencyType.init(exactAmount: 125), 372 | "operator failed", 373 | file: file, line: line 374 | ) 375 | 376 | let second = currencyType.init(exactAmount: Decimal(string: "33.33")!) 377 | XCTAssertEqual( 378 | second.dividing(by: -1.1 as Decimal), 379 | currencyType.init(exactAmount: -30.3), 380 | "dividing(by:) failed", 381 | file: file, line: line 382 | ) 383 | 384 | var third = second 385 | third.divide(by: 3.3 as Decimal) 386 | XCTAssertEqual( 387 | third, 388 | currencyType.init(exactAmount: 10.1), 389 | "divide(by:) failed", 390 | file: file, line: line 391 | ) 392 | } 393 | 394 | assertDivisionIsCorrect(for: USD.self) 395 | assertDivisionIsCorrect(for: JPY.self) 396 | assertDivisionIsCorrect(for: KWD.self) 397 | } 398 | } 399 | 400 | // MARK: Example Usage 401 | 402 | extension CurrencyValueArithmeticTests { 403 | func test_sampleUSDTransaction_withTaxRate_isNotMissingPennies() { 404 | /* 405 | original price taxes (9%) result 406 | 3.00 0.27 3.27 407 | 2.99 0.2691 => 0.27 3.2591 => 3.26 408 | 5.98 0.5382 => 0.54 6.5182 => 6.52 409 | === === === 410 | 11.97 1.0773 => 1.08 13.0473 => 13.05 411 | */ 412 | let prices: [USD] = [ 413 | 3.00, 414 | 2.99, 415 | 5.98 416 | ] 417 | 418 | let subtotal = prices.sum() 419 | XCTAssertEqual(subtotal.exactAmount, 11.97) 420 | 421 | let taxes = prices.sum { $0 * 0.09 } 422 | XCTAssertEqual(taxes.exactAmount, 1.0773) 423 | XCTAssertEqual(taxes.roundedAmount, 1.08) 424 | XCTAssertEqual(subtotal * 0.09, taxes) 425 | 426 | let grandTotal = subtotal + taxes 427 | XCTAssertEqual(grandTotal.exactAmount, 13.0473) 428 | XCTAssertEqual(grandTotal.roundedAmount, 13.05) 429 | } 430 | 431 | func test_sampleUSDTransaction_withTaxRate_andDiscount_isNotMissingPennies() { 432 | /* .228735 433 | original price discount (15%) discount price taxes (9%) result 434 | 3.00 0.45 2.55 0.2295 => 0.23 2.7795 => 2.78 435 | 2.99 0.4485 => 0.45 2.5415 => 2.54 0.228735 => 0.23 2.770235 => 2.77 436 | 5.98 0.897 => 0.90 5.083 => 5.08 0.45747 => 0.46 5.54047 => 5.54 437 | === === === === === 438 | 11.97 1.7955 => 1.80 10.1745 => 10.17 0.915705 => 0.92 11.090205 => 11.09 439 | */ 440 | let prices: [USD] = [ 441 | 3.00, 442 | 2.99, 443 | 5.98 444 | ] 445 | 446 | let discountPrices = prices.map { $0 - ($0 * 0.15) } 447 | 448 | let discountSubtotal = discountPrices.sum() 449 | XCTAssertEqual(discountSubtotal.exactAmount, 10.1745) 450 | XCTAssertEqual(discountSubtotal.roundedAmount, 10.17) 451 | 452 | let discountTaxes = discountPrices.sum { $0 * 0.09 } 453 | XCTAssertEqual(discountTaxes.exactAmount, 0.915705) 454 | XCTAssertEqual(discountTaxes.roundedAmount, Decimal(string: "0.92")!) 455 | 456 | let grandTotal = discountSubtotal + discountTaxes 457 | XCTAssertEqual(grandTotal.exactAmount, 11.090205) 458 | XCTAssertEqual(grandTotal.roundedAmount, 11.09) 459 | } 460 | 461 | func test_sampleUSDHotelBookingTransaction() { 462 | /* 463 | Decimal | USD (rounding at each step) 464 | Base Price: 199.98 | 199.98 465 | ---- 466 | 6% Discount: 11.9988 | 11.9988 => 12.00 467 | Running Total: 187.9812 | 187.98 468 | ---- 469 | 9% Tax: 16.918308 | 16.9182 => 16.92 470 | Running Total: 204.899508 | 204.90 471 | ---- 472 | Franchise fee: 5.68 | 5.68 473 | Running Total: 210.579508 | 210.58 474 | ---- 475 | Total (7 days) 1,474.056556 | 1,474.06 476 | ---- 477 | 10% Commission 147.4056556 | 147.406 => 147.41 478 | Grand Total 1,621.4622116 | 1,621.466 => 1,621.47 479 | */ 480 | let roomDailyRate = USD(199.98) 481 | let discountRate = Decimal(0.06) 482 | let taxRate = Decimal(0.09) 483 | let flatFranchiseFee = USD(5.68) 484 | 485 | var runningDailyTotal = roomDailyRate 486 | 487 | // apply discount 488 | let discount = roomDailyRate * discountRate 489 | XCTAssertEqual(discount.roundedAmount, 12) 490 | runningDailyTotal -= discount 491 | XCTAssertEqual(runningDailyTotal.roundedAmount, 187.98) 492 | 493 | // apply taxes 494 | let taxes = runningDailyTotal * taxRate 495 | XCTAssertEqual(taxes.roundedAmount, Decimal(string: "16.92")!) 496 | runningDailyTotal += taxes 497 | XCTAssertEqual(runningDailyTotal.roundedAmount, 204.90) 498 | 499 | // apply flat fee 500 | runningDailyTotal += flatFranchiseFee 501 | XCTAssertEqual(runningDailyTotal.roundedAmount, 210.58) 502 | 503 | // calculate week total 504 | let weekRateTotal = runningDailyTotal * 7 505 | XCTAssertEqual(weekRateTotal.roundedAmount, 1_474.06) 506 | 507 | // calculate commission 508 | let commission = weekRateTotal * 0.10 509 | XCTAssertEqual(commission.roundedAmount, 147.41) 510 | 511 | let totalPrice = weekRateTotal + commission 512 | XCTAssertEqual(totalPrice.roundedAmount, 1_621.46) 513 | 514 | // Decimal validation 515 | let expectedResult: Decimal = { 516 | let basePrice = roomDailyRate.exactAmount 517 | let discount = basePrice * 0.06 518 | let tax = (basePrice - discount) * 0.09 519 | let dayPrice = basePrice - discount + tax + 5.68 520 | let weekPrice = dayPrice * 7 521 | let commission = weekPrice * 0.10 522 | return weekPrice + commission 523 | }() 524 | XCTAssertEqual(expectedResult, 1_621.4622116) 525 | 526 | XCTAssertEqual(totalPrice, USD(exactAmount: expectedResult)) 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/CurrencyValue+StringRepresentationTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Currency 16 | import XCTest 17 | 18 | final class CurrencyValueStringRepresentationTests: XCTestCase { 19 | let sampleDollar = USD(300.8) 20 | let sampleYen = JPY(400.98) 21 | } 22 | 23 | // MARK: Reflection Description 24 | 25 | extension CurrencyValueStringRepresentationTests { 26 | func test_reflectionRepresentation_includesIdentifierName() { 27 | XCTAssertTrue(String(reflecting: self.sampleDollar).contains(USD.alphabeticCode)) 28 | XCTAssertTrue(String(reflecting: self.sampleYen).contains(JPY.alphabeticCode)) 29 | } 30 | 31 | func test_reflectionRepresentation_includesExactAmount() { 32 | XCTAssertTrue(String(reflecting: self.sampleDollar).contains(self.sampleDollar.exactAmount.description)) 33 | XCTAssertTrue(String(reflecting: self.sampleYen).contains(self.sampleYen.exactAmount.description)) 34 | } 35 | 36 | func test_customMirror_children_includes_exactAmount() throws { 37 | let child = try XCTUnwrap( 38 | Mirror(reflecting: self.sampleDollar) 39 | .children 40 | .first(where: { $0.label == "exactAmount" }) 41 | ) 42 | XCTAssertEqual(child.value as? Decimal, self.sampleDollar.exactAmount) 43 | } 44 | 45 | func test_customMirror_children_includes_minorUnits() throws { 46 | let child = try XCTUnwrap( 47 | Mirror(reflecting: self.sampleDollar) 48 | .children 49 | .first(where: { $0.label == "minorUnits" }) 50 | ) 51 | XCTAssertEqual(child.value as? CurrencyMinorUnitRepresentation, self.sampleDollar.minorUnits) 52 | } 53 | 54 | func test_customMirror_children_includes_descriptor() throws { 55 | let child = try XCTUnwrap( 56 | Mirror(reflecting: self.sampleDollar) 57 | .children 58 | .first(where: { $0.label == "descriptor" }) 59 | ) 60 | XCTAssertNotNil(child.value as? USD.Type) 61 | } 62 | } 63 | 64 | // MARK: String Description 65 | 66 | extension CurrencyValueStringRepresentationTests { 67 | func test_description_includesIdentifierName() { 68 | XCTAssertTrue(self.sampleDollar.description.contains(USD.alphabeticCode)) 69 | XCTAssertTrue(self.sampleYen.description.contains(JPY.alphabeticCode)) 70 | } 71 | 72 | func test_description_includesExactAmount() { 73 | XCTAssertTrue(self.sampleDollar.description.contains(self.sampleDollar.exactAmount.description)) 74 | XCTAssertTrue(self.sampleYen.description.contains(self.sampleYen.exactAmount.description)) 75 | } 76 | } 77 | 78 | // MARK: Debug Description 79 | 80 | extension CurrencyValueStringRepresentationTests { 81 | func test_debugDescription_includesIdentifierName() { 82 | XCTAssertTrue(self.sampleDollar.debugDescription.contains(USD.alphabeticCode)) 83 | XCTAssertTrue(self.sampleYen.debugDescription.contains(JPY.alphabeticCode)) 84 | } 85 | 86 | func test_debugDescription_includesExactAmount() { 87 | XCTAssertTrue(self.sampleDollar.debugDescription.contains(self.sampleDollar.exactAmount.description)) 88 | XCTAssertTrue(self.sampleYen.debugDescription.contains(self.sampleYen.exactAmount.description)) 89 | } 90 | 91 | func test_debugDescription_includesMinorUnits() { 92 | XCTAssertTrue(self.sampleDollar.debugDescription.contains(self.sampleDollar.minorUnits.description)) 93 | XCTAssertTrue(self.sampleYen.debugDescription.contains(self.sampleYen.minorUnits.description)) 94 | } 95 | } 96 | 97 | // MARK: Playground Description 98 | 99 | extension CurrencyValueStringRepresentationTests { 100 | func test_playgroundDescription_matchesDebugDescription() { 101 | XCTAssertEqual(self.sampleDollar.playgroundDescription as? String, self.sampleDollar.debugDescription) 102 | XCTAssertEqual(self.sampleYen.playgroundDescription as? String, self.sampleYen.debugDescription) 103 | } 104 | } 105 | 106 | // MARK: String Interpolation 107 | 108 | extension CurrencyValueStringRepresentationTests { 109 | func test_stringInterpolation_forLocale_whenNilValue_usesNilDescription_whenProvided() { 110 | let value: USD? = nil 111 | XCTAssertEqual("\(localize: value, nilDescription: #function)", #function) 112 | } 113 | 114 | func test_stringInterpolation_withFormatter_whenNilValue_usesNilDescription_whenProvided() { 115 | let value: USD? = nil 116 | XCTAssertEqual("\(localize: value, with: NumberFormatter(), nilDescription: #function)", #function) 117 | } 118 | 119 | func test_stringInterpolation_forLocale_whenNilValue_hasDefaultDescription() { 120 | let value: USD? = nil 121 | XCTAssertEqual("\(localize: value)", "nil") 122 | } 123 | 124 | func test_stringInterpolation_withFormatter_whenNilValue_hasDefaultDescription() { 125 | let value: USD? = nil 126 | XCTAssertEqual("\(localize: value, with: NumberFormatter())", "nil") 127 | } 128 | 129 | func test_stringInterpolation_forLocale_usesLocale_whenProvided() { 130 | let pounds = GBP(14928.789) 131 | XCTAssertEqual("\(localize: pounds, for: .init(identifier: "en_UK"))", "£14,928.79") 132 | XCTAssertEqual("\(localize: pounds, for: .init(identifier: "de_DE"))", "14.928,79 £") 133 | } 134 | 135 | func test_stringInterpolation_withFormatter_usesFormatter_whenProvided() { 136 | let formatter = NumberFormatter() 137 | formatter.numberStyle = .currency 138 | formatter.currencyGroupingSeparator = " " 139 | formatter.currencyDecimalSeparator = "'" 140 | formatter.locale = .init(identifier: "en_US") 141 | 142 | let pounds = GBP(14928.018) 143 | formatter.currencyCode = GBP.alphabeticCode 144 | XCTAssertEqual("\(localize: pounds, with: formatter)", "£14 928'02") 145 | 146 | let expectedYenResult = "¥4 001" 147 | let yen = JPY(4000.9) 148 | formatter.currencyCode = JPY.alphabeticCode 149 | XCTAssertEqual("\(localize: yen, with: formatter)", expectedYenResult) 150 | } 151 | 152 | func test_stringInterpolation_forLocale_usesDefaultLocale() { 153 | let formatter = self.defaultLocaleNumberFormatter 154 | formatter.currencyCode = USD.alphabeticCode 155 | let expectedOutput = formatter.string(for: Decimal(4321.389)) 156 | XCTAssertEqual("\(localize: USD(4321.389))", expectedOutput) 157 | } 158 | } 159 | 160 | // MARK: LocalizedString 161 | 162 | extension CurrencyValueStringRepresentationTests { 163 | func test_localizedString_forLocale_usesDefaultLocale() { 164 | let formatter = self.defaultLocaleNumberFormatter 165 | formatter.currencyCode = USD.alphabeticCode 166 | 167 | let expectedOutput = formatter.string(for: Decimal(4321.389)) 168 | XCTAssertEqual(expectedOutput, USD(4321.389).localizedString()) 169 | } 170 | 171 | func test_localizedString_forLocale_usesProvidedLocale() { 172 | let pounds = GBP(14928.789) 173 | XCTAssertEqual(pounds.localizedString(for: .init(identifier: "en_UK")), "£14,928.79") 174 | XCTAssertEqual(pounds.localizedString(for: .init(identifier: "de_DE")), "14.928,79 £") 175 | } 176 | 177 | func test_localizedString_withFormatter_usesProvidedFormatter() { 178 | let formatter = NumberFormatter() 179 | formatter.numberStyle = .currency 180 | formatter.currencyGroupingSeparator = " " 181 | formatter.currencyDecimalSeparator = "'" 182 | formatter.currencyCode = GBP.alphabeticCode 183 | formatter.locale = .init(identifier: "en_US") 184 | 185 | let pounds = GBP(14928.018) 186 | XCTAssertEqual(pounds.localizedString(using: formatter), "£14 928'02") 187 | 188 | let expectedYenResult = "¥4 001" 189 | let yen = JPY(4000.9) 190 | formatter.currencyCode = JPY.alphabeticCode 191 | XCTAssertEqual(yen.localizedString(using: formatter), expectedYenResult) 192 | } 193 | } 194 | 195 | extension CurrencyValueStringRepresentationTests { 196 | private var defaultLocaleNumberFormatter: NumberFormatter { 197 | let formatter = NumberFormatter() 198 | formatter.numberStyle = .currency 199 | formatter.locale = .current 200 | return formatter 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/CurrencyValueTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024-2025 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Currency 16 | import XCTest 17 | 18 | final class CurrencyValueTests: XCTestCase { } 19 | 20 | // MARK: Descriptor 21 | 22 | extension CurrencyValueTests { 23 | func test_instanceDescriptor_matchesStaticDescriptor() { 24 | let value: any CurrencyValue = USD(3.01) 25 | XCTAssertTrue(value.descriptor == USD.descriptor) 26 | } 27 | } 28 | 29 | // MARK: Initialization 30 | 31 | extension CurrencyValueTests { 32 | func test_init_exactAmount_doesNotModifyValue() { 33 | func assertInit(amount: Decimal, for currencyType: (some CurrencyValue).Type, file: StaticString = #filePath, line: UInt = #line) { 34 | XCTAssertEqual( 35 | currencyType.init(exactAmount: amount).exactAmount, 36 | amount, 37 | file: file, line: line 38 | ) 39 | } 40 | 41 | assertInit(amount: 30.23, for: USD.self) 42 | assertInit(amount: -208.001, for: USD.self) 43 | assertInit(amount: .nan, for: USD.self) 44 | 45 | assertInit(amount: 100.23, for: JPY.self) 46 | assertInit(amount: -39820, for: JPY.self) 47 | assertInit(amount: .nan, for: JPY.self) 48 | 49 | assertInit(amount: 02838.29708, for: KWD.self) 50 | assertInit(amount: -300.87, for: KWD.self) 51 | assertInit(amount: .nan, for: KWD.self) 52 | } 53 | 54 | func test_init_minorUnits_correctlyConvertsToDecimal() { 55 | XCTAssertEqual(USD(minorUnits: 100), USD(exactAmount: 1.0)) 56 | XCTAssertEqual(USD(minorUnits: 101), USD(exactAmount: 1.01)) 57 | XCTAssertEqual(USD(minorUnits: 50011), USD(exactAmount: 500.11)) 58 | } 59 | } 60 | 61 | // MARK: Minor Units Representation 62 | 63 | extension CurrencyValueTests { 64 | func test_0MinorUnits_representationIsCorrect() { 65 | XCTAssertEqual(JPY.zero.minorUnits, .zero) 66 | XCTAssertEqual(JPY(exactAmount: 10.01).minorUnits, 10) 67 | XCTAssertEqual(JPY(exactAmount: 100).minorUnits, 100) 68 | } 69 | 70 | func test_1MinorUnits_representationIsCorrect() { 71 | XCTAssertEqual(XTS.zero.minorUnits, .zero) 72 | XCTAssertEqual(XTS(exactAmount: 10.01).minorUnits, 100) 73 | XCTAssertEqual(XTS(exactAmount: 100).minorUnits, 1000) 74 | } 75 | 76 | func test_2MinorUnits_representationIsCorrect() { 77 | XCTAssertEqual(USD.zero.minorUnits, .zero) 78 | XCTAssertEqual(USD(exactAmount: 10.01).minorUnits, 1001) 79 | XCTAssertEqual(USD(exactAmount: 100).minorUnits, 10000) 80 | } 81 | 82 | func test_3MinorUnits_representationIsCorrect() { 83 | XCTAssertEqual(KWD.zero.minorUnits, .zero) 84 | XCTAssertEqual(KWD(exactAmount: 10.01).minorUnits, 10010) 85 | XCTAssertEqual(KWD(exactAmount: 100).minorUnits, 100000) 86 | } 87 | } 88 | 89 | // MARK: Equatable, Hashable, Comparable 90 | 91 | extension CurrencyValueTests { 92 | struct TestCurrency: CurrencyValue, CurrencyDescriptor { 93 | static var name: String { "TestCurrency" } 94 | static var alphabeticCode: String { "TC" } 95 | static var numericCode: UInt16 { .max } 96 | static var minorUnits: UInt8 { 2 } 97 | 98 | let exactAmount: Decimal 99 | } 100 | 101 | func test_equatable_whenDifferentDescriptors_isFalse() { 102 | XCTAssertFalse(USD(exactAmount: .nan) == TestCurrency(exactAmount: .nan)) 103 | } 104 | 105 | func test_equatable_whenSameDescriptors_andSameExactAmount_isTrue() { 106 | XCTAssertTrue(TestCurrency(exactAmount: .nan) == TestCurrency(exactAmount: .nan)) 107 | } 108 | 109 | func test_equatable_whenSameDescriptors_andDifferentExactAmount_isFalse() { 110 | XCTAssertFalse(TestCurrency(exactAmount: .nan) == TestCurrency(exactAmount: .zero)) 111 | XCTAssertFalse(TestCurrency(exactAmount: 30.01) == TestCurrency(exactAmount: 30.001)) 112 | XCTAssertFalse(TestCurrency(exactAmount: 30.01) == TestCurrency(exactAmount: 30.012)) 113 | XCTAssertFalse(TestCurrency(exactAmount: 30.01) == TestCurrency(exactAmount: 30.019)) 114 | } 115 | 116 | func test_comparable_whenSameDescriptors_comparesExactAmount() { 117 | XCTAssertTrue(TestCurrency(exactAmount: 30) < TestCurrency(exactAmount: 30.01)) 118 | } 119 | 120 | func test_hashable_whenDifferentDescriptors_hasDifferentHashValues() { 121 | let first = self._hash_currency(TestCurrency(exactAmount: 30)) 122 | let second = self._hash_currency(USD(exactAmount: 30)) 123 | 124 | XCTAssertNotEqual(first, second) 125 | } 126 | 127 | func test_hashable_whenSameDescriptors_andSameExactAmount_hasSameHashValues() { 128 | let first = self._hash_currency(TestCurrency(exactAmount: 30)) 129 | let second = self._hash_currency(TestCurrency(exactAmount: 30)) 130 | 131 | XCTAssertEqual(first, second) 132 | } 133 | 134 | func test_hashable_whenSameDescriptors_andDifferentExactAmount_hasDifferentHasValues() { 135 | let first = self._hash_currency(TestCurrency(exactAmount: 30)) 136 | let second = self._hash_currency(TestCurrency(exactAmount: 30.01)) 137 | 138 | XCTAssertNotEqual(first, second) 139 | } 140 | 141 | private func _hash_currency(_ currency: some CurrencyValue) -> Int { 142 | var hasher = Hasher() 143 | hasher.combine(currency) 144 | return hasher.finalize() 145 | } 146 | } 147 | 148 | // MARK: Example Usage 149 | 150 | extension CurrencyValueTests { 151 | func test_existential_mathCompiles() { 152 | let value: any CurrencyValue = USD(minorUnits: 300) 153 | 154 | XCTAssertTrue(value is USD) 155 | XCTAssertEqual(value.adding(3.5).exactAmount, 6.5) 156 | } 157 | 158 | func test_existential_isImplicitlyOpened() { 159 | func someGenericContext(_ value: some CurrencyValue) -> Bool { 160 | return value is USD 161 | } 162 | func someOtherGenericContext(_ value: Currency) -> Currency { 163 | return value + 3.5 164 | } 165 | 166 | let value: any CurrencyValue = USD.zero 167 | XCTAssertTrue(someGenericContext(value)) 168 | XCTAssertEqual(someOtherGenericContext(value).exactAmount, 3.5) 169 | } 170 | 171 | func test_cryptocurrencyDefinition_doesNotCrash() { 172 | struct ETH: CurrencyValue, CurrencyDescriptor { 173 | public static var name: String { "Eth" } 174 | public static var alphabeticCode: String { "ETH" } 175 | public static var numericCode: UInt16 { 999 } 176 | public static var minorUnits: UInt8 { 18 } 177 | 178 | let exactAmount: Decimal 179 | 180 | init(exactAmount: Decimal) { self.exactAmount = exactAmount } 181 | } 182 | 183 | XCTAssertEqual(ETH(5.01).exactAmount, 5.01) 184 | 185 | let value = ETH(exactAmount: Decimal(string: "0.000000000000000001")!) 186 | XCTAssertEqual(value.distributedEvenly(intoParts: 1).first, value) 187 | } 188 | } 189 | 190 | // MARK: Documentation Samples 191 | 192 | extension CurrencyValueTests { 193 | struct USGas: CurrencyValue, CurrencyDescriptor { 194 | public static var name: String { return "US Gas" } 195 | public static var alphabeticCode: String { return "USGas" } 196 | public static var numericCode: UInt16 { return 8401 } // prefixed with the USD numericCode 197 | public static var minorUnits: UInt8 { return 3 } 198 | 199 | let exactAmount: Decimal 200 | 201 | init(exactAmount: Decimal) { self.exactAmount = exactAmount } 202 | } 203 | 204 | func test_customCurrenciesArticle_customCurrencyDefinition_isCorrect() { 205 | let chevronPrice = USGas(3.2689) 206 | XCTAssertEqual(chevronPrice.exactAmount, 3.2689) 207 | XCTAssertEqual(chevronPrice.roundedAmount, 3.269) 208 | } 209 | 210 | func test_customCurrenciesArticle_customMintExample_isCorrect() { 211 | let customMint = CurrencyMint(fallbackLookup: { identifier in 212 | guard 213 | identifier == .alphaCode(USGas.alphabeticCode.uppercased()) || identifier == .numericCode(USGas.numericCode) 214 | else { 215 | return nil 216 | } 217 | 218 | return USGas.self 219 | }) 220 | 221 | let chevronPrice = customMint.make(identifier: "USGas", exactAmount: 3.023) 222 | XCTAssertTrue(type(of: chevronPrice) == (any CurrencyValue)?.self) 223 | XCTAssertEqual(chevronPrice?.description, "3.023 USGas") 224 | } 225 | 226 | func test_currencyMathematicsArticle_naiveAlgorithmExample_isCorrect() { 227 | let total = USD(15.01) 228 | let numPatrons = 3 229 | let individualTotal = total.exactAmount / Decimal(numPatrons) 230 | let splitValues: [USD] = .init( 231 | repeating: .init(exactAmount: individualTotal), 232 | count: numPatrons 233 | ) 234 | 235 | XCTAssertEqual(splitValues.map(\.roundedAmount), [5, 5, 5]) 236 | } 237 | 238 | func test_currencyMathematicsArticle_algorithmExample_isCorrect() { 239 | let total = USD(15.01) 240 | let splitValues = total.distributedEvenly(intoParts: 3) 241 | 242 | XCTAssertEqual(splitValues.map(\.roundedAmount.description), ["5.01", "5", "5"]) 243 | } 244 | 245 | func test_currencyValueType_floatLiteralExample_isCorrect() { 246 | let incorrectValue = USD(exactAmount: 62.12) 247 | XCTAssertEqual(incorrectValue.exactAmount, 62.11999999999998976) 248 | 249 | let amount = Decimal(string: "62.12")! 250 | let correctValue = USD(exactAmount: amount) 251 | XCTAssertEqual(correctValue.exactAmount.description, "62.12") 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Tests/CurrencyTests/Sequence+CurrencyTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftCurrency open source project 4 | // 5 | // Copyright (c) 2024 SwiftCurrency project authors 6 | // Licensed under MIT License 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftCurrency project authors 10 | // 11 | // SPDX-License-Identifier: MIT 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Currency 16 | import XCTest 17 | 18 | final class SequenceCurrencyTests: XCTestCase { } 19 | 20 | // MARK: Sum 21 | 22 | extension SequenceCurrencyTests { 23 | func test_sum() { 24 | let amounts: [USD] = [304.98, 19.02] 25 | let sumTotal = amounts.sum() 26 | XCTAssertEqual(sumTotal.roundedAmount, 324) 27 | } 28 | 29 | func test_sum_withWhereClause() { 30 | let amounts: [USD] = [304.98, 9.02, 30.21] 31 | let sumTotal = amounts.sum(where: { $0.exactAmount > 20 }) 32 | XCTAssertEqual(sumTotal.roundedAmount, 335.19) 33 | } 34 | 35 | func test_sum_withMap() { 36 | let prices: [USD] = [3, 2.99, 5.98] 37 | let totalTaxes = prices.sum { $0 * Decimal(0.09) } 38 | XCTAssertEqual(totalTaxes.roundedAmount, 1.08) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/generate_contributors_list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##===----------------------------------------------------------------------===## 3 | ## 4 | ## This source file is part of the Currency open source project 5 | ## 6 | ## Copyright (c) 2020 Currency project authors 7 | ## Licensed under MIT License 8 | ## 9 | ## See LICENSE.txt for license information 10 | ## See CONTRIBUTORS.txt for the list of Currency project authors 11 | ## 12 | ## SPDX-License-Identifier: MIT 13 | ## 14 | ##===----------------------------------------------------------------------===## 15 | ##===----------------------------------------------------------------------===## 16 | ## 17 | ## This source file is part of the SwiftNIO open source project 18 | ## 19 | ## Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors 20 | ## Licensed under Apache License v2.0 21 | ## 22 | ## See LICENSE.txt for license information 23 | ## See CONTRIBUTORS.txt for the list of SwiftNIO project authors 24 | ## 25 | ## SPDX-License-Identifier: Apache-2.0 26 | ## 27 | ##===----------------------------------------------------------------------===## 28 | 29 | set -eu 30 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 31 | contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) 32 | 33 | cat > "$here/../CONTRIBUTORS.txt" <<- EOF 34 | For the purpose of tracking copyright, this is the list of individuals and 35 | organizations who have contributed source code to SwiftCurrency. 36 | 37 | For employees of an organization/company where the copyright of work done 38 | by employees of that company is held by the company itself, only the company 39 | needs to be listed here. 40 | 41 | ## COPYRIGHT HOLDERS 42 | 43 | - Peek Travel Inc. (all contributors with '@peek.com') 44 | 45 | ### Contributors 46 | 47 | $contributors 48 | 49 | **Updating this list** 50 | 51 | Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` 52 | EOF 53 | --------------------------------------------------------------------------------