├── .credo.exs ├── .editorconfig ├── .formatter.exs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request.md └── workflows │ ├── elixir.yml │ └── shiftleft-analysis.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── config └── config.exs ├── lib ├── ex_gtin.ex └── validation │ └── validation.ex ├── mix.exs ├── mix.lock └── test ├── ex_gtin_test.exs ├── test_helper.exs └── validation └── validation_test.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | {Credo.Check.Design.TagFIXME, []}, 88 | # You can also customize the exit_status of each check. 89 | # If you don't want TODO comments to cause `mix credo` to fail, just 90 | # set this value to 0 (zero). 91 | # 92 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FilterCount, []}, 126 | {Credo.Check.Refactor.FilterFilter, []}, 127 | {Credo.Check.Refactor.FunctionArity, []}, 128 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 129 | {Credo.Check.Refactor.MapJoin, []}, 130 | {Credo.Check.Refactor.MatchInCondition, []}, 131 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 132 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 133 | {Credo.Check.Refactor.Nesting, []}, 134 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.UnlessWithElse, []}, 137 | {Credo.Check.Refactor.WithClauses, []}, 138 | 139 | # 140 | ## Warnings 141 | # 142 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 143 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 144 | {Credo.Check.Warning.Dbg, []}, 145 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 146 | {Credo.Check.Warning.IExPry, []}, 147 | {Credo.Check.Warning.IoInspect, []}, 148 | {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, 149 | {Credo.Check.Warning.OperationOnSameValues, []}, 150 | {Credo.Check.Warning.OperationWithConstantResult, []}, 151 | {Credo.Check.Warning.RaiseInsideRescue, []}, 152 | {Credo.Check.Warning.SpecWithStruct, []}, 153 | {Credo.Check.Warning.UnsafeExec, []}, 154 | {Credo.Check.Warning.UnusedEnumOperation, []}, 155 | {Credo.Check.Warning.UnusedFileOperation, []}, 156 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 157 | {Credo.Check.Warning.UnusedListOperation, []}, 158 | {Credo.Check.Warning.UnusedPathOperation, []}, 159 | {Credo.Check.Warning.UnusedRegexOperation, []}, 160 | {Credo.Check.Warning.UnusedStringOperation, []}, 161 | {Credo.Check.Warning.UnusedTupleOperation, []}, 162 | {Credo.Check.Warning.WrongTestFileExtension, []} 163 | ], 164 | disabled: [ 165 | # 166 | # Checks scheduled for next check update (opt-in for now) 167 | {Credo.Check.Refactor.UtcNowTruncate, []}, 168 | 169 | # 170 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 171 | # and be sure to use `mix credo --strict` to see low priority checks) 172 | # 173 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 174 | {Credo.Check.Consistency.UnusedVariableNames, []}, 175 | {Credo.Check.Design.DuplicatedCode, []}, 176 | {Credo.Check.Design.SkipTestWithoutComment, []}, 177 | {Credo.Check.Readability.AliasAs, []}, 178 | {Credo.Check.Readability.BlockPipe, []}, 179 | {Credo.Check.Readability.ImplTrue, []}, 180 | {Credo.Check.Readability.MultiAlias, []}, 181 | {Credo.Check.Readability.NestedFunctionCalls, []}, 182 | {Credo.Check.Readability.OneArityFunctionInPipe, []}, 183 | {Credo.Check.Readability.OnePipePerLine, []}, 184 | {Credo.Check.Readability.SeparateAliasRequire, []}, 185 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 186 | {Credo.Check.Readability.SinglePipe, []}, 187 | {Credo.Check.Readability.Specs, []}, 188 | {Credo.Check.Readability.StrictModuleLayout, []}, 189 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 190 | {Credo.Check.Refactor.ABCSize, []}, 191 | {Credo.Check.Refactor.AppendSingleItem, []}, 192 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 193 | {Credo.Check.Refactor.FilterReject, []}, 194 | {Credo.Check.Refactor.IoPuts, []}, 195 | {Credo.Check.Refactor.MapMap, []}, 196 | {Credo.Check.Refactor.ModuleDependencies, []}, 197 | {Credo.Check.Refactor.NegatedIsNil, []}, 198 | {Credo.Check.Refactor.PassAsyncInTestCases, []}, 199 | {Credo.Check.Refactor.PipeChainStart, []}, 200 | {Credo.Check.Refactor.RejectFilter, []}, 201 | {Credo.Check.Refactor.VariableRebinding, []}, 202 | {Credo.Check.Warning.LazyLogging, []}, 203 | {Credo.Check.Warning.LeakyEnvironment, []}, 204 | {Credo.Check.Warning.MapGetUnsafePass, []}, 205 | {Credo.Check.Warning.MixEnv, []}, 206 | {Credo.Check.Warning.UnsafeToAtom, []} 207 | 208 | # {Credo.Check.Refactor.MapInto, []}, 209 | 210 | # 211 | # Custom checks can be created using `mix credo.gen.check`. 212 | # 213 | ] 214 | } 215 | } 216 | ] 217 | } 218 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | charset = utf-8 12 | 13 | [{!*.md,!*.markdown}] 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | about: Contribute to our project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the changes** 11 | A clear and concise description of what the change is. 12 | 13 | ## PR Type 14 | What kind of change does this PR introduce? 15 | ``` 16 | [ ] Bugfix 17 | [ ] Feature 18 | [ ] Code style update (formatting, local variables) 19 | [ ] Refactoring (no functional changes, no api changes) 20 | [ ] Build related changes 21 | [ ] CI related changes 22 | [ ] Documentation content changes 23 | [ ] Tests 24 | [ ] Other 25 | ``` 26 | 27 | ## What's new? 28 | - 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master # Or your default branch 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Elixir 17 | uses: erlef/setup-beam@v1 18 | with: 19 | elixir-version: "1.15" 20 | otp-version: "26" 21 | 22 | - name: Install dependencies 23 | run: mix deps.get 24 | 25 | - name: Compile project 26 | run: mix compile --warnings-as-errors 27 | 28 | - name: Run tests 29 | env: 30 | MIX_ENV: test 31 | run: mix test 32 | -------------------------------------------------------------------------------- /.github/workflows/shiftleft-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates Scan with GitHub's code scanning feature 2 | # Scan is a free open-source security tool for modern DevOps teams from ShiftLeft 3 | # Visit https://slscan.io/en/latest/integrations/code-scan for help 4 | name: SL Scan 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [ master ] 12 | schedule: 13 | - cron: '25 16 * * 3' 14 | 15 | jobs: 16 | Scan-Build: 17 | # Scan runs on ubuntu, mac and windows 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v1 21 | # Instructions 22 | # 1. Setup JDK, Node.js, Python etc depending on your project type 23 | # 2. Compile or build the project before invoking scan 24 | # Example: mvn compile, or npm install or pip install goes here 25 | # 3. Invoke Scan with the github token. Leave the workspace empty to use relative url 26 | 27 | - name: Perform Scan 28 | uses: ShiftLeftSecurity/scan-action@master 29 | env: 30 | WORKSPACE: "" 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | SCAN_AUTO_BUILD: true 33 | with: 34 | output: reports 35 | # Scan auto-detects the languages in your project. To override uncomment the below variable and set the type 36 | # type: credscan,java 37 | # type: python 38 | 39 | - name: Upload report 40 | uses: github/codeql-action/upload-sarif@v1 41 | with: 42 | sarif_file: reports 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | *.tar 23 | /.elixir_ls 24 | 25 | .DS_Store -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 26.2 2 | elixir 1.15.8 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.2.0] - 2025-04-18 9 | 10 | - Updated min Elixir version to 1.15. Tested successfully with 1.18.3, 1.16, and 1.15. 11 | - Fixed incorrect GS1 Code handling as noted in issue `#2` 12 | - Fixed normalize/1 function to add correct GTIN-14 indicator digit prefix as a default of `1` 13 | - Updated corresponding DocTests and tests 14 | - Added tests and corrected bad tests that were validating bad checks 15 | - Added tests for edge cases and checks for error handling 16 | - Fixed incorrect `@spec` for `validate!` 17 | - Update README.md with better formatting and refreshed notes 18 | - Added `.formatter.exs` 19 | - 20 | 21 | ## [1.1.0] - 2022-01-11 22 | 23 | - Adding test coverage 24 | - Fixing conflicting GS1 Codes 99 to 990 for "GS1 coupon identification" 25 | - Bump minumum version of Elixir to `1.12` 26 | - Update dependencies 27 | - Replace deprecated `use Mix.Config` to `import Mix.Config` 28 | - Add Pull Request template - @cdesch 29 | - Updating security Policy - (Not sure that we need this but whatever) - @cdesch 30 | - Fixing "contributions welcome" badge link - @cdesch 31 | - Replacing CI badges on README.md - @cdesch 32 | - Adding github ci for automated testing and removing semaphoreci - @cdesch 33 | 34 | ## [1.0.2] - 2021-03-14 35 | 36 | Summary: Merged new function `normalize/1` and other refactoring thanks to the fork [fork](https://github.com/hellonarrativ/ex_gtin) 37 | and @michaeljguarino 38 | 39 | Details: 40 | 41 | - Merged [7a1a0fc3f](https://github.com/hellonarrativ/ex_gtin/commit/7a1a0fc3f42f9eacd5de61f24cac0b9f3e52d1a7) from [fork](https://github.com/hellonarrativ/ex_gtin) - @cdesch 42 | - Adding `normalize/1` and tests to convert a GTIN or ISBN to GTIN-14 format - Big Thanks to @michaeljguarino! 43 | - Update `gtin_check_digit`, `generate_gtin_code` to use capture operators `&` - Big Thanks to @michaeljguarino! 44 | - Add `()` to `generate_check_digit` functions - Big Thanks to @michaeljguarino! 45 | - Fix Formatting of `multiply_and_sum_array`, `subtract_from_nearest_multiple_of_ten`, `mult_by_index_code` and `find_gs1_prefix_country` - Big Thanks to @michaeljguarino! 46 | - Added tests for GTIN-8, GTIN-12, GTIN 14 - @cdesch 47 | 48 | ## [1.0.1] - 2021-03-14 49 | 50 | - Bumping version from `1.0.0` to `1.0.1` - @cdesch 51 | - Update dependenciens `credo`, `excoveralls` and `ex_doc` to the latest versions - @cdesch 52 | - Add installation instructions to readme.md - @cdesch 53 | - Testing with elixir 1.11.3 - @cdesch 54 | - Remove deprecated functions `check_gtin` and `generate_gtin` - _Please use `validate/1` and `generate/1` instead_ - @cdesch 55 | - Remove tests associated with `check_gtin` and `generate_gtin` - _Please use `validate/1` and `generate/1` instead_ - @cdesch 56 | - Convert `@since` to `@doc since:` for `ex_doc` - @cdesch 57 | - Add proper `@doc since: "1.0.0"` to `validation.ex` - @cdesch 58 | - Add `preferred_cli_env` as `:test` for `pull_request_checkout.task` task - @cdesch 59 | 60 | ## [1.0.0] - 2019-08-06 61 | 62 | ### Contains breaking changes\* 63 | 64 | - _BREAKING CHANGE_ `generate/1` - Formerly would return the result. It now returns the result in an atom e.g. `{:ok, "6291041500213"}` - @cdesch 65 | - Added `generate!/1` - Raises `ArgumentError` if invalid - @cdesch 66 | - Added `validate!/1`- Raises `ArgumentError` if invalid - @cdesch 67 | - Deprecated `generate_gtin` for `generate`. `generate_gtin` will be removed in version `1.0.1` - @cdesch 68 | - Deprecated `check_gtin` for `validate`. `check_gtin` will be removed in version `1.0.1` - @cdesch 69 | - Updated README with changes 70 | - Fixed README markdown issues for code indentation 71 | 72 | ## [0.4.0] - 2019-07-26 73 | 74 | - Deprecated `generate_gtin` for `generate`. `generate_gtin` will be removed in version `1.0.0` - @cdesch 75 | - Deprecated `check_gtin` for `validated`. `check_gtin` will be removed in version `1.0.0` - @cdesch 76 | - Validated Functionality with Elixir 1.9.1 and Elixir 1.7.4 - @cdesch 77 | - README Updates with additional information - @cdesch 78 | - Credo Fixes - @cdesch 79 | - Updating Credo from `0.10.0` to `1.1.2` - @cdesch 80 | - Updating Coveralls from `0.9.2` to `0.11.1` - @cdesch 81 | - Updating ExDocs from `0.19.1` to `0.21.1` - @cdesch 82 | 83 | TODO: Make functions private in the validation module 84 | 85 | ## [0.3.4] - 2018-08-15 (Not Published) 86 | 87 | - Adding `describe` groupings to tests - @cdesch 88 | 89 | ## [0.3.3] - 2018-08-15 90 | 91 | - Reformatted CHANGELOG.md - @cdesch 92 | - Testing with Elixir 1.7.2 - @cdesch 93 | - Added UPC acronym definition to README.md - @cdesch 94 | - Updating Credo from `0.9.2` to `0.10.0` - @cdesch 95 | - Updating Coveralls from `0.8.2` to `0.9.2` - @cdesch 96 | - Updating ExDocs from `0.18.3` to `0.19.1` - @cdesch 97 | 98 | ## [0.3.2] - 2018-05-21 99 | 100 | - Adding UPC to description in README.md - @cdesch 101 | - Adding Module Docs - @cdesch 102 | - Fixing Readme Link for MIT license badge - @cdesch 103 | 104 | ## [0.3.1] - 2018-05-21 105 | 106 | - Updated dependencies and fixed Credo Errors - @cdesch 107 | 108 | ## [0.3.0] - 2018-01-16 109 | 110 | - Added GS1 Prefix Look up `gs1_prefix_country`for country code - @cdesch 111 | 112 | ## [0.2.7] - 2018-01-16 113 | 114 | - Bumping version to 0.2.7 due to issue with `mix hex.publish` - @cdesch 115 | 116 | ## [0.2.6] - 2018-01-16 117 | 118 | - Bumping versions of credo, ex_doc and coveralls - @cdesch 119 | 120 | ## [0.2.5] - 2017-07-30 121 | 122 | - Added additional Doc Tests - @cdesch 123 | 124 | ## [0.2.4] - 2017-07-28 125 | 126 | - Changed package name from ExGtin to ex_gtin - @cdesch 127 | 128 | ## [0.2.3] - 2017-07-28 129 | 130 | - Fixing [README.md](README.md) formatting - @cdesch 131 | - Refactored `string` type spec to `String.t()` - @cdesch 132 | - Added composite mix task for validating the library - @cdesch 133 | - Changed [CONTRIBUTING.md](CONTRIBUTING.md) pull request process to test the library - @cdesch 134 | 135 | ### Added 136 | 137 | - Added .editorconfig file - @cdesch 138 | - Added more test for each type of GTIN - @cdesch 139 | 140 | ## [0.2.2] - 2017-07-06 141 | 142 | - Added Generate GTIN function - @cdesch 143 | - Added CHANGELOG.md with history - @cdesch 144 | - Updated Readme with usage and minor fixes - @cdesch 145 | 146 | ## [0.2.1] - 2017-07-04 147 | 148 | - Added GTIN Length Validation and error handling - @cdesch 149 | 150 | ## [0.2.0] - 2017-07-03 151 | 152 | - Added [CONTRIBUTING.md](CONTRIBUTING.md) file - @cdesch 153 | - Added [LICENSE.md](LICENSE.md) file - @cdesch 154 | - Reorganizing code in modules - @cdesch 155 | 156 | ## [0.1.0] - 2017-07-03 157 | 158 | - Initial Release - @cdesch 159 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@kickinespresso.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the [CHANGELOG.md](CHANGELOG.md) with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the [README.md](README.md) to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. Run the tests (`ExUnit`), code coverage (`coveralls`), static analysis tools (`credo`), and code formatting as follows: 17 | 18 | ```shell 19 | mix test 20 | MIX_ENV=test mix coveralls 21 | mix credo --strict 22 | mix format --check-formatted 23 | ``` 24 | 25 | or 26 | 27 | ```shell 28 | mix pull_request_checkout.task 29 | ``` 30 | 31 | ## Code of Conduct 32 | 33 | ### Our Pledge 34 | 35 | In the interest of fostering an open and welcoming environment, we as 36 | contributors and maintainers pledge to making participation in our project and 37 | our community a harassment-free experience for everyone, regardless of age, body 38 | size, disability, ethnicity, gender identity and expression, level of experience, 39 | nationality, personal appearance, race, religion, or sexual identity and 40 | orientation. 41 | 42 | ### Our Standards 43 | 44 | Examples of behavior that contributes to creating a positive environment 45 | include: 46 | 47 | - Using welcoming and inclusive language 48 | - Being respectful of differing viewpoints and experiences 49 | - Gracefully accepting constructive criticism 50 | - Focusing on what is best for the community 51 | - Showing empathy towards other community members 52 | 53 | Examples of unacceptable behavior by participants include: 54 | 55 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 56 | - Trolling, insulting/derogatory comments, and personal or political attacks 57 | - Public or private harassment 58 | - Publishing others' private information, such as a physical or electronic 59 | address, without explicit permission 60 | - Other conduct which could reasonably be considered inappropriate in a 61 | professional setting 62 | 63 | ### Our Responsibilities 64 | 65 | Project maintainers are responsible for clarifying the standards of acceptable 66 | behavior and are expected to take appropriate and fair corrective action in 67 | response to any instances of unacceptable behavior. 68 | 69 | Project maintainers have the right and responsibility to remove, edit, or 70 | reject comments, commits, code, wiki edits, issues, and other contributions 71 | that are not aligned to this Code of Conduct, or to ban temporarily or 72 | permanently any contributor for other behaviors that they deem inappropriate, 73 | threatening, offensive, or harmful. 74 | 75 | ### Scope 76 | 77 | This Code of Conduct applies both within project spaces and in public spaces 78 | when an individual is representing the project or its community. Examples of 79 | representing a project or community include using an official project e-mail 80 | address, posting via an official social media account, or acting as an appointed 81 | representative at an online or offline event. Representation of a project may be 82 | further defined and clarified by project maintainers. 83 | 84 | ### Enforcement 85 | 86 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 87 | reported by contacting the project team at [contact@kickinespresso.com](mailto:contact@kickinespresso.com). All 88 | complaints will be reviewed and investigated and will result in a response that 89 | is deemed necessary and appropriate to the circumstances. The project team is 90 | obligated to maintain confidentiality with regard to the reporter of an incident. 91 | Further details of specific enforcement policies may be posted separately. 92 | 93 | Project maintainers who do not follow or enforce the Code of Conduct in good 94 | faith may face temporary or permanent repercussions as determined by other 95 | members of the project's leadership. 96 | 97 | ### Attribution 98 | 99 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 100 | available at [http://contributor-covenant.org/version/1/4][version] 101 | 102 | [homepage]: http://contributor-covenant.org 103 | [version]: http://contributor-covenant.org/version/1/4/ 104 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # ex_gtin License 2 | 3 | Copyright 2017-2021 Kickin Espresso 4 | 5 | License Type: MIT 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExGtin 2 | 3 | ![CI Status](https://github.com/kickinespresso/ex_gtin/actions/workflows/elixir.yml/badge.svg) 4 | [![Static Badge](https://img.shields.io/badge/HexDocs-ex_gtin-blue)](https://hexdocs.pm/ex_gtin/ExGtin.html) 5 | [![Hex.pm Version](https://img.shields.io/hexpm/v/ex_gtin)](https://hex.pm/packages/ex_gtin) 6 | [![Packagist](https://img.shields.io/packagist/l/doctrine/orm.svg)](LICENSE.md) 7 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/kickinespresso/ex_gtin/issues) 8 | 9 | A [GTIN](https://www.gtin.info/) (Global Trade Item Number) & UPC (Universal Price Code) Generation and Validation Library in Elixir under the GS1 specification. 10 | 11 | - GTIN-8 (EAN/UCC-8): this is an 8-digit number used predominately outside of North America 12 | - GTIN-12 (UPC-A): this is a 12-digit number used primarily in North America 13 | - GTIN-13 (EAN/UCC-13): this is a 13-digit number used predominately outside of North America - Global Location Number (GLN) 14 | - GTIN-14 (EAN/UCC-14 or ITF-14): this is a 14-digit number used to identify trade items at various packaging levels 15 | 16 | ## Features 17 | 18 | - Supports GTIN-8, GTIN-12 (UPC-12), GTIN-13 (GLN), GTIN-14 19 | - Generate GTIN 20 | - Check GTIN validity 21 | - Lookup GS1 country prefix 22 | - Convert (normalize) GTIN-13 to GTIN-14 23 | 24 | Features to Come: 25 | 26 | - Global Shipment Identification Number (GSIN) 27 | - Serial Shipping Container Code (SSCC) 28 | 29 | ## Installation 30 | 31 | _WARNING `1.0.1` contains breaking changes from `1.0.0`_ (I know this is a patch release but the breaking changes related to the deprecation of `check_gtin` and `generate_gtin` were noted in the changelog and docs over a year ago) 32 | _WARNING `1.0.0` contains breaking changes from `0.4.0`_ 33 | 34 | Add `:ex_gtin` as a dependency to your project's `mix.exs`: 35 | 36 | ```elixir 37 | def deps do 38 | [{:ex_gtin, "~> 1.2.0"}] 39 | end 40 | ``` 41 | 42 | and run `mix deps.get` to install the `:ex_gtin` dependency 43 | 44 | ```shell 45 | mix deps.get 46 | ``` 47 | 48 | ## Usage 49 | 50 | - Check GTIN codes 51 | 52 | ```elixir 53 | iex> ExGtin.validate("6291041500213") 54 | {:ok, "GTIN-13"} 55 | 56 | iex> ExGtin.validate("6291041500214") 57 | {:error, "Invalid Code"} 58 | 59 | iex> ExGtin.validate!("6291041500213") 60 | "GTIN-13" 61 | ``` 62 | 63 | Pass GTIN numbers in as a String, Number or an Array 64 | 65 | ```elixir 66 | iex> number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3] 67 | iex> ExGtin.validate(number) 68 | {:ok, "GTIN-13"} 69 | 70 | iex> number = 6_291_041_500_213 71 | iex> ExGtin.validate(number) 72 | {:ok, "GTIN-13"} 73 | ``` 74 | 75 | - Generate GTIN codes 76 | 77 | ```elixir 78 | iex> ExGtin.generate("629104150021") 79 | {:ok, "6291041500213"} 80 | 81 | iex> ExGtin.generate!("629104150021") 82 | "6291041500213" 83 | ``` 84 | 85 | - Lookup GS1 Prefix 86 | 87 | ```elixir 88 | iex> ExGtin.Validation.find_gs1_prefix_country("53523235") 89 | {:ok, "GS1 Malta"} 90 | ``` 91 | 92 | - Convert GTIN-13 to GTIN 14 93 | 94 | ```elixir 95 | iex> ExGtin.normalize("6291041500213") 96 | {:ok, "16291041500210"} 97 | ``` 98 | 99 | ### Using Strings, Arrays or Numbers 100 | 101 | - String 102 | 103 | ```elixir 104 | iex> ExGtin.validate("6291041500213") 105 | {:ok, "GTIN-13"} 106 | ``` 107 | 108 | - Array of Integers 109 | 110 | ```elixir 111 | iex> ExGtin.validate([6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3]) 112 | {:ok, "GTIN-13"} 113 | ``` 114 | 115 | - Integer 116 | 117 | ```elixir 118 | iex> ExGtin.validate(6291041500213) 119 | {:ok, "GTIN-13"} 120 | ``` 121 | 122 | Integers with leading zeros may not process properly 123 | 124 | ## Reference 125 | 126 | - [GTIN](https://www.gs1.org) 127 | - [How to calculate GTIN](https://www.gs1.org/how-calculate-check-digit-manually) 128 | 129 | Documentation can be found at [https://hexdocs.pm/ex_gtin](https://hexdocs.pm/ex_gtin) on [HexDocs](https://hexdocs.pm). 130 | 131 | ## Tests 132 | 133 | Run tests with 134 | 135 | ```shell 136 | mix test 137 | ``` 138 | 139 | Run test coverage 140 | 141 | ```shell 142 | MIX_ENV=test mix coveralls 143 | ``` 144 | 145 | Produce Coverage report in HTML to `cover/excoveralls.html` 146 | 147 | ```shell 148 | MIX_ENV=test mix coveralls.html 149 | ``` 150 | 151 | ## Contributing 152 | 153 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 154 | 155 | When making pull requests, please be sure to update the [CHANGELOG.md](CHANGELOG.md) with the corresponding changes. Please make sure that all tests pass, add tests for new functionality and that the static analysis checker `credo` is run. Use `mix pull_request_checkout.task` to ensure that everything checks out. 156 | 157 | Run static code analysis 158 | 159 | ```shell 160 | mix credo 161 | ``` 162 | 163 | Generate Documentation 164 | 165 | ```shell 166 | mix docs 167 | ``` 168 | 169 | Run the gambit of tests, static analysis and coverage 170 | 171 | ```shell 172 | mix pull_request_checkout.task 173 | ``` 174 | 175 | ## Sponsors 176 | 177 | This project is sponsored by [KickinEspresso](https://kickinespresso.com/?utm_source=github&utm_medium=sponsor&utm_campaign=opensource) 178 | 179 | ## Versioning 180 | 181 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/kickinespresso/ex_gtin/tags). 182 | 183 | ## Code of Conduct 184 | 185 | Please refer to the [Code of Conduct](CODE_OF_CONDUCT.md) for details 186 | 187 | ## Security 188 | 189 | Please refer to the [Security](SECURITY.md) for details 190 | 191 | ## License 192 | 193 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 194 | 195 | ## Publish & Releasing 196 | 197 | ```shell 198 | mix hex.publish 199 | ``` 200 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.2 | :white_check_mark: | 8 | | 0.4.x | :x: | 9 | | 0.3.x | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please open an issue if this library some how has a security issue. Stranger stuff has happend. 14 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ex_gtin, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ex_gtin, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/ex_gtin.ex: -------------------------------------------------------------------------------- 1 | defmodule ExGtin do 2 | @moduledoc """ 3 | Documentation for ExGtin. This library provides 4 | functionality for validating GTIN compliant codes. 5 | """ 6 | @moduledoc since: "1.0.1" 7 | 8 | import ExGtin.Validation 9 | 10 | @type result :: {:ok, binary} | {:error, binary} 11 | 12 | @doc """ 13 | Check for valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 14 | 15 | Returns `{:ok, "GTIN-#"}` or `{:error}` 16 | 17 | ## Examples 18 | 19 | iex> ExGtin.validate("6291041500213") 20 | {:ok, "GTIN-13"} 21 | 22 | iex> ExGtin.validate("6291041500214") 23 | {:error, "Invalid Code"} 24 | """ 25 | @doc since: "0.4.0" 26 | @spec validate(String.t() | list(number)) :: result 27 | def validate(number) do 28 | gtin_check_digit(number) 29 | end 30 | 31 | @doc """ 32 | Converts a GTIN or ISBN to GTIN-14 format 33 | 34 | ## Examples 35 | 36 | iex> ExGtin.normalize("6291041500213") 37 | {:ok, "16291041500210"} 38 | """ 39 | @doc since: "1.1.0" 40 | @spec normalize(binary | list(number)) :: result 41 | def normalize(gtin) do 42 | with {:ok, type} <- do_gtin_check_digit(gtin), 43 | do: {:ok, normalize_gtin(gtin, type)} 44 | end 45 | 46 | defp do_gtin_check_digit(isbn) when byte_size(isbn) == 10, do: {:ok, "ISBN-10"} 47 | defp do_gtin_check_digit(gtin), do: gtin_check_digit(gtin) 48 | 49 | defp normalize_gtin(gtin, "GTIN-8") do 50 | digits = String.codepoints(gtin) |> Enum.map(&String.to_integer/1) 51 | {code, _} = Enum.split(digits, 7) 52 | "100000#{Enum.join(code)}#{generate_check_digit([1, 0, 0, 0, 0, 0] ++ code)}" 53 | end 54 | 55 | defp normalize_gtin(gtin, "ISBN-10") do 56 | digits = String.codepoints(gtin) |> Enum.map(&String.to_integer/1) 57 | {code, _} = Enum.split(digits, 9) 58 | 59 | "0978#{Enum.join(code)}#{generate_check_digit([9, 7, 8] ++ code)}" 60 | end 61 | 62 | defp normalize_gtin(gtin, "GTIN-12") do 63 | digits = String.codepoints(gtin) |> Enum.map(&String.to_integer/1) 64 | {code, _} = Enum.split(digits, 11) 65 | "10#{Enum.join(code)}#{generate_check_digit([1, 0] ++ code)}" 66 | end 67 | 68 | defp normalize_gtin(gtin, "GTIN-13") do 69 | digits = String.codepoints(gtin) |> Enum.map(&String.to_integer/1) 70 | {code, _} = Enum.split(digits, 12) 71 | "1#{Enum.join(code)}#{generate_check_digit([1] ++ code)}" 72 | end 73 | 74 | defp normalize_gtin(gtin, "GTIN-14"), do: gtin 75 | 76 | @doc """ 77 | Check for valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 78 | 79 | Throws ArgumentError if an error occurs 80 | 81 | ## Examples 82 | 83 | iex> ExGtin.validate!("6291041500213") 84 | "GTIN-13" 85 | """ 86 | @doc since: "1.2.0" 87 | @spec validate!(String.t() | list(number)) :: String.t() 88 | def validate!(number) do 89 | case gtin_check_digit(number) do 90 | {:ok, result} -> result 91 | {:error, reason} -> raise ArgumentError, message: reason 92 | end 93 | end 94 | 95 | @doc """ 96 | Generates valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 97 | 98 | Returns code with check digit 99 | 100 | ## Examples 101 | 102 | iex> ExGtin.generate("629104150021") 103 | {:ok, "6291041500213"} 104 | 105 | iex> ExGtin.generate("62921") 106 | {:error, "Invalid GTIN Code Length"} 107 | 108 | """ 109 | @doc since: "0.4.0" 110 | @spec generate(String.t() | list(number)) :: number | {atom, String.t()} 111 | def generate(number) do 112 | case generate_gtin_code(number) do 113 | {:ok, result} -> {:ok, result} 114 | {:error, reason} -> {:error, reason} 115 | end 116 | end 117 | 118 | @doc """ 119 | Generates valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 120 | 121 | Throws Argument Exception if there was an error 122 | 123 | ## Examples 124 | 125 | iex> ExGtin.generate!("629104150021") 126 | "6291041500213" 127 | 128 | iex> ExGtin.generate!("62921") 129 | ** (ArgumentError) Invalid GTIN Code Length 130 | 131 | """ 132 | @doc since: "1.2.0" 133 | @spec generate!(String.t() | list(number)) :: binary() 134 | def generate!(number) do 135 | case generate_gtin_code(number) do 136 | {:ok, result} -> result 137 | {:error, reason} -> raise ArgumentError, message: reason 138 | end 139 | end 140 | 141 | @doc """ 142 | Find the GS1 prefix country for a GTIN number 143 | 144 | Returns `{atom, String.t()}` 145 | 146 | ## Examples 147 | 148 | iex> ExGtin.gs1_prefix_country("53523235") 149 | {:ok, "GS1 Malta"} 150 | 151 | iex> ExGtin.gs1_prefix_country("6291041500214") 152 | {:ok, "GS1 Emirates"} 153 | 154 | iex> ExGtin.gs1_prefix_country("9541041500214") 155 | {:error, "No GS1 prefix found"} 156 | """ 157 | @doc since: "0.1.0" 158 | @spec gs1_prefix_country(String.t() | list(number)) :: {atom, String.t()} 159 | def gs1_prefix_country(number) do 160 | find_gs1_prefix_country(number) 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/validation/validation.ex: -------------------------------------------------------------------------------- 1 | defmodule ExGtin.Validation do 2 | @moduledoc """ 3 | Documentation for ExGtin. This library provides 4 | functionality for validating GTIN compliant codes. 5 | """ 6 | @moduledoc since: "1.0.0" 7 | 8 | @doc """ 9 | Check for valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 10 | 11 | Returns `{atom, String.t()}` 12 | 13 | ## Examples 14 | 15 | iex> ExGtin.Validation.gtin_check_digit("6291041500213") 16 | {:ok, "GTIN-13"} 17 | 18 | iex> ExGtin.Validation.gtin_check_digit("6291041500214") 19 | {:error, "Invalid Code"} 20 | """ 21 | @doc since: "1.0.0" 22 | @spec gtin_check_digit(String.t()) :: {atom, String.t()} 23 | def gtin_check_digit(number) when is_bitstring(number) do 24 | number 25 | |> String.codepoints() 26 | |> Enum.map(&String.to_integer/1) 27 | |> gtin_check_digit() 28 | end 29 | 30 | @spec gtin_check_digit(number) :: {atom, String.t()} 31 | def gtin_check_digit(number) when is_number(number), 32 | do: gtin_check_digit(Integer.digits(number)) 33 | 34 | @spec gtin_check_digit(list(number)) :: {atom, String.t()} 35 | def gtin_check_digit(number) do 36 | case check_code_length(number) do 37 | {:ok, gtin_type} -> 38 | {code, check_digit} = Enum.split(number, length(number) - 1) 39 | 40 | calculated_check_digit = 41 | code 42 | |> multiply_and_sum_array 43 | |> subtract_from_nearest_multiple_of_ten 44 | 45 | case calculated_check_digit == Enum.at(check_digit, 0) do 46 | true -> {:ok, gtin_type} 47 | _ -> {:error, "Invalid Code"} 48 | end 49 | 50 | {:error, error} -> 51 | {:error, error} 52 | end 53 | end 54 | 55 | @doc """ 56 | Generate valid GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 57 | 58 | Returns `{atom, String.t()}` 59 | 60 | ## Examples 61 | 62 | iex> ExGtin.Validation.generate_gtin_code("629104150021") 63 | {:ok, "6291041500213"} 64 | 65 | iex> ExGtin.Validation.generate_gtin_code("62921") 66 | {:error, "Invalid GTIN Code Length"} 67 | 68 | """ 69 | @doc since: "1.0.0" 70 | @spec generate_gtin_code(String.t()) :: String.t() | {atom, String.t()} 71 | def generate_gtin_code(number) when is_bitstring(number) do 72 | number 73 | |> String.codepoints() 74 | |> Enum.map(&String.to_integer/1) 75 | |> generate_gtin_code() 76 | end 77 | 78 | @spec generate_gtin_code(number) :: String.t() | {atom, String.t()} 79 | def generate_gtin_code(number) when is_number(number), 80 | do: generate_gtin_code(Integer.digits(number)) 81 | 82 | @spec generate_gtin_code(list(number)) :: String.t() | {atom, String.t()} 83 | def generate_gtin_code(number) do 84 | case generate_check_code_length(number) do 85 | {:ok, _} -> 86 | check_digit = generate_check_digit(number) 87 | result = Enum.join(number ++ [check_digit]) 88 | {:ok, result} 89 | 90 | {:error, error} -> 91 | {:error, error} 92 | end 93 | end 94 | 95 | @doc """ 96 | Generate check digit GTIN-8, GTIN-12, GTIN-13, GTIN-14, GSIN, SSCC codes 97 | 98 | Returns `number` 99 | 100 | ## Examples 101 | 102 | iex> ExGtin.Validation.generate_check_digit([6,2,9,1,0,4,1,5,0,0,2,1]) 103 | 3 104 | 105 | """ 106 | @doc since: "1.0.0" 107 | @spec generate_check_digit(list(number)) :: number 108 | def generate_check_digit(number) do 109 | number 110 | |> multiply_and_sum_array() 111 | |> subtract_from_nearest_multiple_of_ten() 112 | end 113 | 114 | @doc """ 115 | Calculates the sum of the digits in a string and multiplied value based on index order 116 | 117 | Returns `number` 118 | 119 | ## Examples 120 | 121 | iex> ExGtin.Validation.multiply_and_sum_array([6,2,9,1,0,4,1,5,0,0,2,1]) 122 | 57 123 | 124 | """ 125 | @doc since: "1.0.0" 126 | @spec multiply_and_sum_array(list(number)) :: number 127 | def multiply_and_sum_array(numbers) do 128 | numbers 129 | |> Enum.reverse() 130 | |> Stream.with_index() 131 | |> Enum.reduce(0, fn {num, idx}, acc -> 132 | acc + num * mult_by_index_code(idx) 133 | end) 134 | end 135 | 136 | @doc """ 137 | Calculates the difference of the highest rounded multiple of 10 138 | 139 | Returns `number` 140 | 141 | ## Examples 142 | 143 | iex> ExGtin.Validation.subtract_from_nearest_multiple_of_ten(57) 144 | 3 145 | 146 | """ 147 | @doc since: "1.0.0" 148 | @spec subtract_from_nearest_multiple_of_ten(number) :: number 149 | def subtract_from_nearest_multiple_of_ten(number), do: rem(10 - rem(number, 10), 10) 150 | 151 | @doc """ 152 | By index, returns the corresponding value to multiply 153 | the digit by 154 | 155 | Returns `number` 156 | 157 | ## Examples 158 | 159 | iex> ExGtin.Validation.mult_by_index_code(1) 160 | 1 161 | 162 | iex> ExGtin.Validation.mult_by_index_code(2) 163 | 3 164 | 165 | """ 166 | @doc since: "1.0.0" 167 | @spec mult_by_index_code(number) :: number 168 | def mult_by_index_code(index) when rem(index, 2) == 1, do: 1 169 | def mult_by_index_code(_), do: 3 170 | 171 | @doc """ 172 | Checks the code for the proper length as specified by the 173 | GTIN-8,12,13,14 specification 174 | 175 | Returns {atom, String.t()} 176 | 177 | ## Examples 178 | 179 | iex> ExGtin.Validation.check_code_length([1,2,3,4,5,6,7,8]) 180 | {:ok, "GTIN-8"} 181 | 182 | iex> ExGtin.Validation.check_code_length([1,2,3,4,5,6,7]) 183 | {:error, "Invalid GTIN Code Length"} 184 | 185 | """ 186 | @doc since: "1.0.0" 187 | @spec check_code_length([number]) :: {atom, String.t()} 188 | def check_code_length(number) do 189 | case length(number) do 190 | 8 -> {:ok, "GTIN-8"} 191 | 12 -> {:ok, "GTIN-12"} 192 | 13 -> {:ok, "GTIN-13"} 193 | 14 -> {:ok, "GTIN-14"} 194 | _ -> {:error, "Invalid GTIN Code Length"} 195 | end 196 | end 197 | 198 | @doc """ 199 | When generating the code, checks the code for 200 | the proper length as specified by the GTIN-8,12,13,14 specification. 201 | The code should be -1 the length of the GTIN code as the check digit 202 | will be added later 203 | 204 | Returns {atom, String.t()} 205 | 206 | ## Examples 207 | 208 | iex> ExGtin.Validation.generate_check_code_length([1,2,3,4,5,6,7]) 209 | {:ok, "GTIN-8"} 210 | 211 | iex> ExGtin.Validation.generate_check_code_length([1,2,3,4,5,6]) 212 | {:error, "Invalid GTIN Code Length"} 213 | 214 | """ 215 | @doc since: "1.0.0" 216 | @spec generate_check_code_length(list(number)) :: {atom, String.t()} 217 | def generate_check_code_length(number), do: check_code_length(number ++ [1]) 218 | 219 | @doc """ 220 | Find the GS1 prefix country for a GTIN number 221 | 222 | Returns `{atom, String.t()}` 223 | 224 | ## Examples 225 | 226 | iex> ExGtin.Validation.find_gs1_prefix_country("53523235") 227 | {:ok, "GS1 Malta"} 228 | 229 | iex> ExGtin.Validation.find_gs1_prefix_country("6291041500214") 230 | {:ok, "GS1 Emirates"} 231 | 232 | iex> ExGtin.Validation.find_gs1_prefix_country("9541041500214") 233 | {:error, "No GS1 prefix found"} 234 | """ 235 | @doc since: "1.0.0" 236 | @spec find_gs1_prefix_country(String.t()) :: {atom, String.t()} 237 | def find_gs1_prefix_country(number) when is_bitstring(number) do 238 | number 239 | |> String.codepoints() 240 | |> Enum.map(&String.to_integer/1) 241 | |> find_gs1_prefix_country() 242 | end 243 | 244 | @spec find_gs1_prefix_country(number) :: {atom, String.t()} 245 | def find_gs1_prefix_country(number) when is_number(number), 246 | do: find_gs1_prefix_country(Integer.digits(number)) 247 | 248 | @spec find_gs1_prefix_country(list(number)) :: {atom, String.t()} 249 | def find_gs1_prefix_country(number) do 250 | case check_code_length(number) do 251 | {:ok, gtin_type} -> 252 | normalized = 253 | case gtin_type do 254 | "GTIN-12" -> [0] ++ number 255 | "GTIN-14" -> Enum.drop(number, 1) 256 | _ -> number 257 | end 258 | 259 | {prefix, _code} = Enum.split(normalized, 3) 260 | 261 | prefix 262 | |> Enum.join() 263 | |> String.to_integer() 264 | |> lookup_gs1_prefix 265 | 266 | {:error, error} -> 267 | {:error, error} 268 | end 269 | end 270 | 271 | @doc """ 272 | Looks up the GS1 prefix in a table 273 | GS1 Reference https://www.gs1.org/company-prefix 274 | 275 | Returns {atom, String.t()} 276 | 277 | """ 278 | @doc since: "1.0.0" 279 | @spec lookup_gs1_prefix(integer) :: {atom, String.t()} 280 | # credo:disable-for-next-line 281 | def lookup_gs1_prefix(number) do 282 | case number do 283 | x when x in 001..019 -> 284 | {:ok, "GS1 US"} 285 | 286 | x when x in 030..039 -> 287 | {:ok, "GS1 US"} 288 | 289 | x when x in 050..059 -> 290 | {:ok, "GS1 US"} 291 | 292 | x when x in 060..099 -> 293 | {:ok, "GS1 US"} 294 | 295 | x when x in 100..139 -> 296 | {:ok, "GS1 US"} 297 | 298 | x when x == 535 -> 299 | {:ok, "GS1 Malta"} 300 | 301 | x when x in 020..029 -> 302 | {:ok, 303 | "Used to issue restricted circulation numbers within a geographic region (MO defined)"} 304 | 305 | x when x in 040..049 -> 306 | {:ok, "Used to issue GS1 restricted circulation numbers within a company"} 307 | 308 | x when x in 200..299 -> 309 | {:ok, 310 | "Used to issue GS1 restricted circulation number within a geographic region (MO defined)"} 311 | 312 | x when x in 300..379 -> 313 | {:ok, "GS1 France"} 314 | 315 | x when x == 380 -> 316 | {:ok, "GS1 Bulgaria"} 317 | 318 | x when x == 383 -> 319 | {:ok, "GS1 Slovenija"} 320 | 321 | x when x == 385 -> 322 | {:ok, "GS1 Croatia"} 323 | 324 | x when x == 387 -> 325 | {:ok, "GS1 BIH (Bosnia-Herzegovina)"} 326 | 327 | x when x == 389 -> 328 | {:ok, "GS1 Montenegro"} 329 | 330 | x when x in 400..440 -> 331 | {:ok, "GS1 Germany"} 332 | 333 | x when x in 450..459 -> 334 | {:ok, "GS1 Japan"} 335 | 336 | x when x in 490..499 -> 337 | {:ok, "GS1 Japan"} 338 | 339 | x when x in 460..469 -> 340 | {:ok, "GS1 Russia"} 341 | 342 | x when x == 470 -> 343 | {:ok, "GS1 Kyrgyzstan"} 344 | 345 | x when x == 471 -> 346 | {:ok, "GS1 Taiwan"} 347 | 348 | x when x == 474 -> 349 | {:ok, "GS1 Estonia"} 350 | 351 | x when x == 475 -> 352 | {:ok, "GS1 Latvia"} 353 | 354 | x when x == 476 -> 355 | {:ok, "GS1 Azerbaijan"} 356 | 357 | x when x == 477 -> 358 | {:ok, "GS1 Lithuania"} 359 | 360 | x when x == 478 -> 361 | {:ok, "GS1 Uzbekistan"} 362 | 363 | x when x == 479 -> 364 | {:ok, "GS1 Sri Lanka"} 365 | 366 | x when x == 480 -> 367 | {:ok, "GS1 Philippines"} 368 | 369 | x when x == 481 -> 370 | {:ok, "GS1 Belarus"} 371 | 372 | x when x == 482 -> 373 | {:ok, "GS1 Ukraine"} 374 | 375 | x when x == 483 -> 376 | {:ok, "GS1 Turkmenistan"} 377 | 378 | x when x == 484 -> 379 | {:ok, "GS1 Moldova"} 380 | 381 | x when x == 485 -> 382 | {:ok, "GS1 Armenia"} 383 | 384 | x when x == 486 -> 385 | {:ok, "GS1 Georgia"} 386 | 387 | x when x == 487 -> 388 | {:ok, "GS1 Kazakstan"} 389 | 390 | x when x == 488 -> 391 | {:ok, "GS1 Tajikistan"} 392 | 393 | x when x == 489 -> 394 | {:ok, "GS1 Hong Kong"} 395 | 396 | x when x in 500..509 -> 397 | {:ok, "GS1 UK"} 398 | 399 | x when x in 520..521 -> 400 | {:ok, "GS1 Association Greece"} 401 | 402 | x when x == 528 -> 403 | {:ok, "GS1 Lebanon"} 404 | 405 | x when x == 529 -> 406 | {:ok, "GS1 Cyprus"} 407 | 408 | x when x == 530 -> 409 | {:ok, "GS1 Albania"} 410 | 411 | x when x == 531 -> 412 | {:ok, "GS1 Macedonia"} 413 | 414 | x when x == 535 -> 415 | {:ok, "GS1 Malta"} 416 | 417 | x when x == 539 -> 418 | {:ok, "GS1 Ireland"} 419 | 420 | x when x in 540..549 -> 421 | {:ok, "GS1 Belgium & Luxembourg"} 422 | 423 | x when x == 560 -> 424 | {:ok, "GS1 Portugal"} 425 | 426 | x when x == 569 -> 427 | {:ok, "GS1 Iceland"} 428 | 429 | x when x in 570..579 -> 430 | {:ok, "GS1 Denmark"} 431 | 432 | x when x == 590 -> 433 | {:ok, "GS1 Poland"} 434 | 435 | x when x == 594 -> 436 | {:ok, "GS1 Romania"} 437 | 438 | x when x == 599 -> 439 | {:ok, "GS1 Hungary"} 440 | 441 | x when x in 600..601 -> 442 | {:ok, "GS1 South Africa"} 443 | 444 | x when x == 603 -> 445 | {:ok, "GS1 Ghana"} 446 | 447 | x when x == 604 -> 448 | {:ok, "GS1 Senegal"} 449 | 450 | x when x == 608 -> 451 | {:ok, "GS1 Bahrain"} 452 | 453 | x when x == 609 -> 454 | {:ok, "GS1 Mauritius"} 455 | 456 | x when x == 611 -> 457 | {:ok, "GS1 Morocco"} 458 | 459 | x when x == 613 -> 460 | {:ok, "GS1 Algeria"} 461 | 462 | x when x == 615 -> 463 | {:ok, "GS1 Nigeria"} 464 | 465 | x when x == 616 -> 466 | {:ok, "GS1 Kenya"} 467 | 468 | x when x == 618 -> 469 | {:ok, "GS1 Ivory Coast"} 470 | 471 | x when x == 619 -> 472 | {:ok, "GS1 Tunisia"} 473 | 474 | x when x == 620 -> 475 | {:ok, "GS1 Tanzania"} 476 | 477 | x when x == 621 -> 478 | {:ok, "GS1 Syria"} 479 | 480 | x when x == 622 -> 481 | {:ok, "GS1 Egypt"} 482 | 483 | x when x == 623 -> 484 | {:ok, "GS1 Brunei"} 485 | 486 | x when x == 624 -> 487 | {:ok, "GS1 Libya"} 488 | 489 | x when x == 625 -> 490 | {:ok, "GS1 Jordan"} 491 | 492 | x when x == 626 -> 493 | {:ok, "GS1 Iran"} 494 | 495 | x when x == 627 -> 496 | {:ok, "GS1 Kuwait"} 497 | 498 | x when x == 628 -> 499 | {:ok, "GS1 Saudi Arabia"} 500 | 501 | x when x == 629 -> 502 | {:ok, "GS1 Emirates"} 503 | 504 | x when x in 640..649 -> 505 | {:ok, "GS1 Finland"} 506 | 507 | x when x in 690..699 -> 508 | {:ok, "GS1 China"} 509 | 510 | x when x in 700..709 -> 511 | {:ok, "GS1 Norway"} 512 | 513 | x when x == 729 -> 514 | {:ok, "GS1 Israel"} 515 | 516 | x when x in 730..739 -> 517 | {:ok, "GS1 Sweden"} 518 | 519 | x when x == 740 -> 520 | {:ok, "GS1 Guatemala"} 521 | 522 | x when x == 741 -> 523 | {:ok, "GS1 El Salvador"} 524 | 525 | x when x == 742 -> 526 | {:ok, "GS1 Honduras"} 527 | 528 | x when x == 743 -> 529 | {:ok, "GS1 Nicaragua"} 530 | 531 | x when x == 744 -> 532 | {:ok, "GS1 Costa Rica"} 533 | 534 | x when x == 745 -> 535 | {:ok, "GS1 Panama"} 536 | 537 | x when x == 746 -> 538 | {:ok, "GS1 Republica Dominicana"} 539 | 540 | x when x == 750 -> 541 | {:ok, "GS1 Mexico"} 542 | 543 | x when x in 754..755 -> 544 | {:ok, "GS1 Canada"} 545 | 546 | x when x == 759 -> 547 | {:ok, "GS1 Venezuela"} 548 | 549 | x when x in 760..769 -> 550 | {:ok, "GS1 Schweiz, Suisse, Svizzera"} 551 | 552 | x when x in 770..771 -> 553 | {:ok, "GS1 Colombia"} 554 | 555 | x when x == 773 -> 556 | {:ok, "GS1 Uruguay"} 557 | 558 | x when x == 775 -> 559 | {:ok, "GS1 Peru"} 560 | 561 | x when x == 777 -> 562 | {:ok, "GS1 Bolivia"} 563 | 564 | x when x in 778..779 -> 565 | {:ok, "GS1 Argentina"} 566 | 567 | x when x == 780 -> 568 | {:ok, "GS1 Chile"} 569 | 570 | x when x == 784 -> 571 | {:ok, "GS1 Paraguay"} 572 | 573 | x when x == 786 -> 574 | {:ok, "GS1 Ecuador"} 575 | 576 | x when x in 789..790 -> 577 | {:ok, "GS1 Brasil"} 578 | 579 | x when x in 800..839 -> 580 | {:ok, "GS1 Italy"} 581 | 582 | x when x in 840..849 -> 583 | {:ok, "GS1 Spain"} 584 | 585 | x when x == 850 -> 586 | {:ok, "GS1 Cuba"} 587 | 588 | x when x == 858 -> 589 | {:ok, "GS1 Slovakia"} 590 | 591 | x when x == 859 -> 592 | {:ok, "GS1 Czech"} 593 | 594 | x when x == 860 -> 595 | {:ok, "GS1 Serbia"} 596 | 597 | x when x == 865 -> 598 | {:ok, "GS1 Mongolia"} 599 | 600 | x when x == 867 -> 601 | {:ok, "GS1 North Korea"} 602 | 603 | x when x in 868..869 -> 604 | {:ok, "GS1 Turkey"} 605 | 606 | x when x in 870..879 -> 607 | {:ok, "GS1 Netherlands"} 608 | 609 | x when x == 880 -> 610 | {:ok, "GS1 South Korea"} 611 | 612 | x when x == 884 -> 613 | {:ok, "GS1 Cambodia"} 614 | 615 | x when x == 885 -> 616 | {:ok, "GS1 Thailand"} 617 | 618 | x when x == 888 -> 619 | {:ok, "GS1 Singapore"} 620 | 621 | x when x == 890 -> 622 | {:ok, "GS1 India"} 623 | 624 | x when x == 893 -> 625 | {:ok, "GS1 Vietnam"} 626 | 627 | x when x == 896 -> 628 | {:ok, "GS1 Pakistan"} 629 | 630 | x when x == 899 -> 631 | {:ok, "GS1 Indonesia"} 632 | 633 | x when x in 900..919 -> 634 | {:ok, "GS1 Austria"} 635 | 636 | x when x in 930..939 -> 637 | {:ok, "GS1 Australia"} 638 | 639 | x when x in 940..949 -> 640 | {:ok, "GS1 New Zealand"} 641 | 642 | x when x == 950 -> 643 | {:ok, "GS1 Global Office"} 644 | 645 | x when x == 951 -> 646 | {:ok, 647 | "Used to issue General Manager Numbers for the EPC General Identifier (GID) scheme as defined by the EPC Tag Data Standard*"} 648 | 649 | x when x == 955 -> 650 | {:ok, "GS1 Malaysia"} 651 | 652 | x when x == 958 -> 653 | {:ok, "GS1 Macau"} 654 | 655 | x when x in 960..969 -> 656 | {:ok, "Global Office (GTIN-8s)*"} 657 | 658 | x when x == 977 -> 659 | {:ok, "Serial publications (ISSN)"} 660 | 661 | x when x in 978..979 -> 662 | {:ok, "Bookland (ISBN)"} 663 | 664 | x when x == 980 -> 665 | {:ok, "Refund receipts"} 666 | 667 | x when x in 981..984 -> 668 | {:ok, "GS1 coupon identification for common currency areas"} 669 | 670 | x when x in 990..999 -> 671 | {:ok, "GS1 coupon identification"} 672 | 673 | _ -> 674 | {:error, "No GS1 prefix found"} 675 | end 676 | end 677 | end 678 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExGtin.Mixfile do 2 | @moduledoc """ 3 | ExGtin `mix.exs` 4 | """ 5 | use Mix.Project 6 | 7 | def project do 8 | [ 9 | app: :ex_gtin, 10 | version: "1.2.0", 11 | elixir: "~> 1.15", 12 | description: description(), 13 | aliases: aliases(), 14 | package: package(), 15 | build_embedded: Mix.env() == :prod, 16 | start_permanent: Mix.env() == :prod, 17 | deps: deps(), 18 | test_coverage: [tool: ExCoveralls], 19 | coverallspreferred_cli_env: [ 20 | "coveralls.detail": :test, 21 | "coveralls.post": :test, 22 | "coveralls.html": :test 23 | ], 24 | preferred_cli_env: [ 25 | "pull_request_checkout.task": :test 26 | ], 27 | # Docs 28 | name: "ExGtin", 29 | source_url: "https://github.com/kickinespresso/ex_gtin", 30 | homepage_url: "https://github.com/kickinespresso/ex_gtin", 31 | docs: [ 32 | main: "ExGtin", 33 | extras: ["README.md"] 34 | ] 35 | ] 36 | end 37 | 38 | # Configuration for the OTP application 39 | # 40 | # Type "mix help compile.app" for more information 41 | def application do 42 | # Specify extra applications you'll use from Erlang/Elixir 43 | [extra_applications: [:logger]] 44 | end 45 | 46 | defp deps do 47 | [ 48 | {:credo, "~> 1.7.11", only: [:dev, :test]}, 49 | {:ex_doc, "~> 0.37.3", only: :dev, runtime: false}, 50 | {:excoveralls, "~> 0.18.5", only: :test} 51 | ] 52 | end 53 | 54 | defp package do 55 | [ 56 | name: "ex_gtin", 57 | maintainers: ["KickinEspresso"], 58 | licenses: ["MIT"], 59 | links: %{"GitHub" => "https://github.com/kickinespresso/ex_gtin"} 60 | ] 61 | end 62 | 63 | defp aliases do 64 | [ 65 | c: "compile", 66 | "pull_request_checkout.task": [ 67 | "test", 68 | "credo --strict", 69 | "coveralls", 70 | "format --check-formatted" 71 | ] 72 | ] 73 | end 74 | 75 | defp description do 76 | """ 77 | Elixir Global Trade Item Number (GTIN) Validation Library for GS1, UPC-12, and GLN. 78 | Validates GTIN-8, GTIN-12 (UPC-12), GTIN-13 (GLN), GTIN-14 codes. 79 | Universal Price Code (UPC) 80 | """ 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, 4 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 5 | "earmark": {:hex, :earmark, "1.3.3", "5e8be428fcef362692b6dbd7dc55bdc7023da26d995cb3fb19aa4bd682bfd3f9", [:mix], [], "hexpm", "a4f21ad675cd496b4b124b00cd7884add73ef836bbfeaa6a85a818df5690a182"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 8 | "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, 9 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, 12 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 13 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 14 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 15 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 17 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 18 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 19 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 21 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 22 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}, 23 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 24 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/ex_gtin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExGtinTest do 2 | @moduledoc """ 3 | Library Tests 4 | """ 5 | use ExUnit.Case 6 | doctest ExGtin 7 | import ExGtin 8 | 9 | @valid_gtin_codes_arrays %{ 10 | codes: [ 11 | [1, 2, 3, 3, 1, 2, 3, 9], 12 | [6, 4, 8, 2, 7, 1, 2, 3, 1, 2, 2, 0], 13 | [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3], 14 | [2, 2, 3, 1, 2, 3, 1, 2, 2, 3, 1, 2, 3, 5], 15 | [2, 2, 3, 1, 2, 3, 1, 2, 2, 3, 1, 2, 3, 5], 16 | [1, 0, 6, 1, 4, 1, 4, 1, 0, 0, 0, 4, 1, 5] 17 | ] 18 | } 19 | 20 | describe "validate/1 function" do 21 | test "with valid number string" do 22 | number = "6291041500213" 23 | assert {:ok, "GTIN-13"} == validate(number) 24 | end 25 | 26 | test "with valid number array" do 27 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3] 28 | assert {:ok, "GTIN-13"} == validate(number) 29 | end 30 | 31 | test "with valid GTIN-8 number" do 32 | number = 50_678_907 33 | assert {:ok, "GTIN-8"} == validate(number) 34 | end 35 | 36 | test "with valid GTIN-12 number" do 37 | number = 614_141_000_449 38 | assert {:ok, "GTIN-12"} == validate(number) 39 | end 40 | 41 | test "with valid GTIN-13 number" do 42 | number = 6_291_041_500_213 43 | assert {:ok, "GTIN-13"} == validate(number) 44 | end 45 | 46 | test "with valid GTIN-14 number" do 47 | number = 10_614_141_000_415 48 | assert {:ok, "GTIN-14"} == validate(number) 49 | end 50 | 51 | test "with invalid number" do 52 | number = "6291041500214" 53 | assert {:error, _} = validate(number) 54 | assert {:error, "Invalid Code"} == validate("6291041533213") 55 | end 56 | 57 | test "with whitespace" do 58 | assert_raise ArgumentError, fn -> 59 | validate(" 6291041500213 ") 60 | end 61 | 62 | assert_raise ArgumentError, fn -> 63 | validate("\t6291041500213\t") 64 | end 65 | end 66 | 67 | test "with non-numeric characters" do 68 | assert_raise ArgumentError, fn -> 69 | validate("629104150021a") 70 | end 71 | 72 | assert_raise ArgumentError, fn -> 73 | validate("629104150021!") 74 | end 75 | end 76 | 77 | test "with empty string" do 78 | assert {:error, "Invalid GTIN Code Length"} == validate("") 79 | end 80 | 81 | test "with very large numbers" do 82 | assert {:error, "Invalid GTIN Code Length"} == validate("999999999999999999999999999999") 83 | end 84 | 85 | test "with negative numbers" do 86 | assert_raise ArgumentError, fn -> 87 | validate("-6291041500213") 88 | end 89 | end 90 | 91 | test "with decimal numbers" do 92 | assert_raise ArgumentError, fn -> 93 | validate("629104150021.3") 94 | end 95 | end 96 | end 97 | 98 | describe "validate!/1 function" do 99 | test "with valid number string" do 100 | number = "6291041500213" 101 | assert "GTIN-13" == validate!(number) 102 | end 103 | 104 | test "with valid number array" do 105 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3] 106 | assert "GTIN-13" == validate!(number) 107 | end 108 | 109 | test "with valid number " do 110 | number = 6_291_041_500_213 111 | assert "GTIN-13" == validate!(number) 112 | end 113 | 114 | test "with invalid number, raises exception" do 115 | number = "6291041500214" 116 | 117 | assert_raise ArgumentError, fn -> 118 | validate!(number) 119 | end 120 | 121 | assert_raise ArgumentError, fn -> 122 | validate!("6291041533213") 123 | end 124 | end 125 | 126 | test "with whitespace raises exception" do 127 | assert_raise ArgumentError, fn -> 128 | validate!(" 6291041500213 ") 129 | end 130 | end 131 | 132 | test "with non-numeric characters raises exception" do 133 | assert_raise ArgumentError, fn -> 134 | validate!("629104150021a") 135 | end 136 | end 137 | 138 | test "with empty string raises exception" do 139 | assert_raise ArgumentError, fn -> 140 | validate!("") 141 | end 142 | end 143 | end 144 | 145 | describe "generate/1 function" do 146 | test "with valid number string" do 147 | number = "629104150021" 148 | assert {:ok, "6291041500213"} == generate(number) 149 | end 150 | 151 | test "with valid number array" do 152 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1] 153 | assert {:ok, "6291041500213"} == generate(number) 154 | end 155 | 156 | test "with valid number " do 157 | number = 629_104_150_021 158 | assert {:ok, "6291041500213"} == generate(number) 159 | end 160 | 161 | test "with whitespace" do 162 | assert_raise ArgumentError, fn -> 163 | generate(" 629104150021 ") 164 | end 165 | end 166 | 167 | test "with non-numeric characters" do 168 | assert_raise ArgumentError, fn -> 169 | generate("629104150021a") 170 | end 171 | end 172 | 173 | test "with empty string" do 174 | assert {:error, "Invalid GTIN Code Length"} == generate("") 175 | end 176 | 177 | test "with very large numbers" do 178 | assert {:error, "Invalid GTIN Code Length"} == generate("999999999999999999999999999999") 179 | end 180 | end 181 | 182 | describe "generate!/1 function" do 183 | test "with valid number string" do 184 | number = "629104150021" 185 | assert "6291041500213" == generate!(number) 186 | end 187 | 188 | test "with invalid number, raises exception" do 189 | assert_raise ArgumentError, fn -> 190 | number = "62921" 191 | IO.puts(generate!(number)) 192 | end 193 | 194 | assert_raise ArgumentError, fn -> 195 | generate!("62921") 196 | end 197 | end 198 | 199 | test "with valid number array" do 200 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1] 201 | assert "6291041500213" == generate!(number) 202 | end 203 | 204 | test "with valid number " do 205 | number = 629_104_150_021 206 | assert "6291041500213" == generate!(number) 207 | end 208 | 209 | test "with whitespace raises exception" do 210 | assert_raise ArgumentError, fn -> 211 | generate!(" 629104150021 ") 212 | end 213 | end 214 | 215 | test "with non-numeric characters raises exception" do 216 | assert_raise ArgumentError, fn -> 217 | generate!("629104150021a") 218 | end 219 | end 220 | 221 | test "with empty string raises exception" do 222 | assert_raise ArgumentError, fn -> 223 | generate!("") 224 | end 225 | end 226 | end 227 | 228 | test "validate all gtin codes" do 229 | Enum.map( 230 | @valid_gtin_codes_arrays[:codes], 231 | fn x -> 232 | assert {:ok, "GTIN-#{length(x)}"} == validate(x) 233 | end 234 | ) 235 | end 236 | 237 | describe "gs1_prefix_country function" do 238 | test "with string" do 239 | number = "53523235" 240 | assert {:ok, "GS1 Malta"} == gs1_prefix_country(number) 241 | end 242 | 243 | test "with number" do 244 | number = 53_523_235 245 | assert {:ok, "GS1 Malta"} == gs1_prefix_country(number) 246 | end 247 | end 248 | 249 | describe "normalize/1 function" do 250 | test "with valid GTIN-8 string" do 251 | number = "40170725" 252 | assert {:ok, "10000040170722"} == normalize(number) 253 | end 254 | 255 | test "with valid ISBN 10 string" do 256 | number = "0205080057" 257 | assert {:ok, "09780205080052"} == normalize(number) 258 | end 259 | 260 | test "with valid GTIN-12 string" do 261 | number = "840030222641" 262 | assert {:ok, "10840030222648"} == normalize(number) 263 | end 264 | 265 | test "with valid GTIN-13 string" do 266 | number = "0840030222641" 267 | assert {:ok, "10840030222648"} == normalize(number) 268 | end 269 | 270 | test "with valid GTIN-14 string" do 271 | number = "10840030222648" 272 | assert {:ok, "10840030222648"} == normalize(number) 273 | end 274 | 275 | test "handles different separator characters" do 276 | assert_raise ArgumentError, fn -> 277 | normalize("401-707-25") 278 | end 279 | 280 | assert_raise ArgumentError, fn -> 281 | normalize("401 707 25") 282 | end 283 | end 284 | end 285 | 286 | describe "README.md examples" do 287 | test "with validate/1 with valid number string" do 288 | number = "6291041500213" 289 | assert {:ok, "GTIN-13"} == validate(number) 290 | end 291 | 292 | test "with validate/1 with invalid number string" do 293 | number = "6291041500214" 294 | assert {:error, "Invalid Code"} == validate(number) 295 | end 296 | 297 | test "with validate!/1 with valid number string" do 298 | number = "6291041500213" 299 | assert "GTIN-13" == validate!(number) 300 | end 301 | 302 | test "with validate/1 with array" do 303 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3] 304 | assert {:ok, "GTIN-13"} == validate(number) 305 | end 306 | 307 | test "with validate/1 with number" do 308 | number = 6_291_041_500_213 309 | assert {:ok, "GTIN-13"} == validate(number) 310 | end 311 | 312 | test "with generate/1" do 313 | number = "629104150021" 314 | assert {:ok, "6291041500213"} == generate(number) 315 | end 316 | 317 | test "with generate!/1" do 318 | number = "629104150021" 319 | assert "6291041500213" == generate!(number) 320 | end 321 | 322 | test "with gs1_prefix_country/1" do 323 | number = "53523235" 324 | assert {:ok, "GS1 Malta"} == gs1_prefix_country(number) 325 | end 326 | 327 | test "with normalize/1" do 328 | assert {:ok, "10000040170722"} == normalize("40170725") 329 | assert {:ok, "16291041500210"} == normalize("6291041500213") 330 | end 331 | end 332 | end 333 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/validation/validation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExGtin.ValidationTest do 2 | @moduledoc """ 3 | Validation Module Test 4 | """ 5 | use ExUnit.Case 6 | doctest ExGtin.Validation 7 | import ExGtin.Validation 8 | 9 | describe "gtin_check_digit function" do 10 | test "with valid number string" do 11 | number = "6291041500213" 12 | assert {:ok, "GTIN-13"} == gtin_check_digit(number) 13 | end 14 | 15 | test "with valid number array" do 16 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1, 3] 17 | assert {:ok, "GTIN-13"} == gtin_check_digit(number) 18 | end 19 | 20 | test "with valid number as integer" do 21 | number = 6_291_041_500_213 22 | assert {:ok, "GTIN-13"} == gtin_check_digit(number) 23 | end 24 | 25 | test "with valid number as string" do 26 | number = "6291041500213" 27 | assert {:ok, "GTIN-13"} == gtin_check_digit(number) 28 | end 29 | 30 | test "with invalid number" do 31 | number = "6291041500214" 32 | assert {:error, "Invalid Code"} == gtin_check_digit(number) 33 | assert {:error, "Invalid Code"} == gtin_check_digit("6291041533213") 34 | end 35 | 36 | test "with invalid length" do 37 | number = "62910415002143232232" 38 | assert {:error, "Invalid GTIN Code Length"} == gtin_check_digit(number) 39 | assert {:error, _} = gtin_check_digit("62910415002143232232") 40 | end 41 | end 42 | 43 | test "mult_by_index_code function" do 44 | assert mult_by_index_code(1) == 1 45 | assert mult_by_index_code(2) == 3 46 | assert mult_by_index_code(3) == 1 47 | assert mult_by_index_code(4) == 3 48 | end 49 | 50 | test "multiply_and_sum_array function" do 51 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1] 52 | assert 57 == multiply_and_sum_array(number) 53 | end 54 | 55 | test "generate_check_digit function" do 56 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1] 57 | assert 3 == generate_check_digit(number) 58 | end 59 | 60 | test "subtract_from_nearest_multiple_of_ten function" do 61 | assert 3 == subtract_from_nearest_multiple_of_ten(57) 62 | end 63 | 64 | test "check_code_length function with valid codes" do 65 | codes = [ 66 | [1, 2, 3, 4, 5, 6, 7, 8], 67 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 68 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], 69 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] 70 | ] 71 | 72 | Enum.map(codes, fn x -> assert {:ok, "GTIN-#{length(x)}"} == check_code_length(x) end) 73 | end 74 | 75 | test "check_code_length function with invalid code" do 76 | code = [1, 2, 3, 4, 5, 6, 7] 77 | assert {:error, _} = check_code_length(code) 78 | end 79 | 80 | test "generate_check_code_length function with valid codes" do 81 | codes = [ 82 | [1, 2, 3, 4, 5, 6, 7], 83 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 84 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 85 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] 86 | ] 87 | 88 | Enum.map(codes, fn x -> 89 | assert {:ok, "GTIN-#{length(x) + 1}"} == generate_check_code_length(x) 90 | end) 91 | end 92 | 93 | test "generate_check_code_length function with invalid code" do 94 | code = [1, 2, 3, 4, 5, 6] 95 | assert {:error, _} = generate_check_code_length(code) 96 | end 97 | 98 | describe "generate_gtin_code/1 function" do 99 | test "with valid number string" do 100 | number = "629104150021" 101 | assert {:ok, "6291041500213"} == generate_gtin_code(number) 102 | end 103 | 104 | test "with valid number array" do 105 | number = [6, 2, 9, 1, 0, 4, 1, 5, 0, 0, 2, 1] 106 | assert {:ok, "6291041500213"} == generate_gtin_code(number) 107 | end 108 | 109 | test "with valid number " do 110 | number = 629_104_150_021 111 | assert {:ok, "6291041500213"} == generate_gtin_code(number) 112 | end 113 | end 114 | 115 | describe "find_gs1_prefix_country function" do 116 | test "with valid number" do 117 | number = "53523235" 118 | assert {:ok, "GS1 Malta"} == find_gs1_prefix_country(number) 119 | end 120 | 121 | test "with invalid prefix code" do 122 | number = "00023235" 123 | assert {:error, "No GS1 prefix found"} == find_gs1_prefix_country(number) 124 | end 125 | 126 | test "with invalid number" do 127 | number = "00035" 128 | assert {:error, "Invalid GTIN Code Length"} == find_gs1_prefix_country(number) 129 | end 130 | 131 | test "with GTIN-12 to add leading zero" do 132 | test_cases = [ 133 | {"012345678905", "GTIN-12", "GS1 US"} 134 | ] 135 | 136 | Enum.each(test_cases, fn {number, format, expected} -> 137 | result = find_gs1_prefix_country(number) 138 | 139 | assert result == {:ok, expected}, 140 | "Expected #{format} #{number} to resolve to #{expected}, but got #{inspect(result)}" 141 | end) 142 | end 143 | 144 | test "with GTIN-12, GTIN-13 and GTIN-14 for US formats for same GS1 prefix" do 145 | test_cases = [ 146 | {"012345678905", "GTIN-12", "GS1 US"}, 147 | {"614141234561", "GTIN-13", "GS1 US"}, 148 | {"012345678905", "GTIN-13", "GS1 US"}, 149 | {"00400051091017", "GTIN-14", 150 | "Used to issue GS1 restricted circulation numbers within a company"}, 151 | {"00500051091014", "GTIN-14", "GS1 US"} 152 | ] 153 | 154 | Enum.each(test_cases, fn {number, format, expected} -> 155 | result = find_gs1_prefix_country(number) 156 | 157 | assert result == {:ok, expected}, 158 | "Expected #{format} #{number} to resolve to #{expected}, but got #{inspect(result)}" 159 | end) 160 | end 161 | 162 | test "with GTIN-13 and GTIN-14 for Germany formats for same GS1 prefix" do 163 | test_cases = [ 164 | {"4051234567896", "GTIN-13", "GS1 Germany"}, 165 | {"4006381333931", "GTIN-13", "GS1 Germany"}, 166 | {"14006381333931", "GTIN-14", "GS1 Germany"}, 167 | {"94051234567896", "GTIN-14", "GS1 Germany"} 168 | ] 169 | 170 | Enum.each(test_cases, fn {number, format, expected} -> 171 | result = find_gs1_prefix_country(number) 172 | 173 | assert result == {:ok, expected}, 174 | "Expected #{format} #{number} to resolve to #{expected}, but got #{inspect(result)}" 175 | end) 176 | end 177 | 178 | test "with GTIN-14 strip leading zero for Germany formats" do 179 | test_cases = [ 180 | {"04000510910178", "GTIN-14", "GS1 Germany"} 181 | ] 182 | 183 | Enum.each(test_cases, fn {number, format, expected} -> 184 | result = find_gs1_prefix_country(number) 185 | 186 | assert result == {:ok, expected}, 187 | "Expected #{format} #{number} to resolve to #{expected}, but got #{inspect(result)}" 188 | end) 189 | end 190 | end 191 | 192 | test "lookup_gs1_prefix test country codes" do 193 | assert lookup_gs1_prefix(001) == {:ok, "GS1 US"} 194 | assert lookup_gs1_prefix(030) == {:ok, "GS1 US"} 195 | assert lookup_gs1_prefix(001) == {:ok, "GS1 US"} 196 | assert lookup_gs1_prefix(050) == {:ok, "GS1 US"} 197 | assert lookup_gs1_prefix(060) == {:ok, "GS1 US"} 198 | assert lookup_gs1_prefix(100) == {:ok, "GS1 US"} 199 | assert lookup_gs1_prefix(535) == {:ok, "GS1 Malta"} 200 | 201 | assert lookup_gs1_prefix(020) == 202 | {:ok, 203 | "Used to issue restricted circulation numbers within a geographic region (MO defined)"} 204 | 205 | assert lookup_gs1_prefix(040) == 206 | {:ok, "Used to issue GS1 restricted circulation numbers within a company"} 207 | 208 | assert lookup_gs1_prefix(200) == 209 | {:ok, 210 | "Used to issue GS1 restricted circulation number within a geographic region (MO defined)"} 211 | 212 | assert lookup_gs1_prefix(300) == {:ok, "GS1 France"} 213 | assert lookup_gs1_prefix(380) == {:ok, "GS1 Bulgaria"} 214 | assert lookup_gs1_prefix(383) == {:ok, "GS1 Slovenija"} 215 | assert lookup_gs1_prefix(385) == {:ok, "GS1 Croatia"} 216 | assert lookup_gs1_prefix(387) == {:ok, "GS1 BIH (Bosnia-Herzegovina)"} 217 | assert lookup_gs1_prefix(389) == {:ok, "GS1 Montenegro"} 218 | assert lookup_gs1_prefix(400) == {:ok, "GS1 Germany"} 219 | assert lookup_gs1_prefix(450) == {:ok, "GS1 Japan"} 220 | assert lookup_gs1_prefix(490) == {:ok, "GS1 Japan"} 221 | assert lookup_gs1_prefix(460) == {:ok, "GS1 Russia"} 222 | assert lookup_gs1_prefix(470) == {:ok, "GS1 Kyrgyzstan"} 223 | assert lookup_gs1_prefix(471) == {:ok, "GS1 Taiwan"} 224 | assert lookup_gs1_prefix(474) == {:ok, "GS1 Estonia"} 225 | assert lookup_gs1_prefix(475) == {:ok, "GS1 Latvia"} 226 | assert lookup_gs1_prefix(476) == {:ok, "GS1 Azerbaijan"} 227 | assert lookup_gs1_prefix(477) == {:ok, "GS1 Lithuania"} 228 | assert lookup_gs1_prefix(478) == {:ok, "GS1 Uzbekistan"} 229 | assert lookup_gs1_prefix(479) == {:ok, "GS1 Sri Lanka"} 230 | assert lookup_gs1_prefix(480) == {:ok, "GS1 Philippines"} 231 | assert lookup_gs1_prefix(481) == {:ok, "GS1 Belarus"} 232 | assert lookup_gs1_prefix(482) == {:ok, "GS1 Ukraine"} 233 | assert lookup_gs1_prefix(483) == {:ok, "GS1 Turkmenistan"} 234 | assert lookup_gs1_prefix(484) == {:ok, "GS1 Moldova"} 235 | assert lookup_gs1_prefix(485) == {:ok, "GS1 Armenia"} 236 | assert lookup_gs1_prefix(486) == {:ok, "GS1 Georgia"} 237 | assert lookup_gs1_prefix(487) == {:ok, "GS1 Kazakstan"} 238 | assert lookup_gs1_prefix(488) == {:ok, "GS1 Tajikistan"} 239 | assert lookup_gs1_prefix(489) == {:ok, "GS1 Hong Kong"} 240 | assert lookup_gs1_prefix(500) == {:ok, "GS1 UK"} 241 | assert lookup_gs1_prefix(520) == {:ok, "GS1 Association Greece"} 242 | assert lookup_gs1_prefix(528) == {:ok, "GS1 Lebanon"} 243 | assert lookup_gs1_prefix(529) == {:ok, "GS1 Cyprus"} 244 | assert lookup_gs1_prefix(530) == {:ok, "GS1 Albania"} 245 | assert lookup_gs1_prefix(531) == {:ok, "GS1 Macedonia"} 246 | assert lookup_gs1_prefix(535) == {:ok, "GS1 Malta"} 247 | assert lookup_gs1_prefix(539) == {:ok, "GS1 Ireland"} 248 | assert lookup_gs1_prefix(540) == {:ok, "GS1 Belgium & Luxembourg"} 249 | assert lookup_gs1_prefix(560) == {:ok, "GS1 Portugal"} 250 | assert lookup_gs1_prefix(569) == {:ok, "GS1 Iceland"} 251 | assert lookup_gs1_prefix(570) == {:ok, "GS1 Denmark"} 252 | assert lookup_gs1_prefix(590) == {:ok, "GS1 Poland"} 253 | assert lookup_gs1_prefix(594) == {:ok, "GS1 Romania"} 254 | assert lookup_gs1_prefix(599) == {:ok, "GS1 Hungary"} 255 | assert lookup_gs1_prefix(600) == {:ok, "GS1 South Africa"} 256 | assert lookup_gs1_prefix(603) == {:ok, "GS1 Ghana"} 257 | assert lookup_gs1_prefix(604) == {:ok, "GS1 Senegal"} 258 | assert lookup_gs1_prefix(608) == {:ok, "GS1 Bahrain"} 259 | assert lookup_gs1_prefix(609) == {:ok, "GS1 Mauritius"} 260 | assert lookup_gs1_prefix(611) == {:ok, "GS1 Morocco"} 261 | assert lookup_gs1_prefix(613) == {:ok, "GS1 Algeria"} 262 | assert lookup_gs1_prefix(615) == {:ok, "GS1 Nigeria"} 263 | assert lookup_gs1_prefix(616) == {:ok, "GS1 Kenya"} 264 | assert lookup_gs1_prefix(618) == {:ok, "GS1 Ivory Coast"} 265 | assert lookup_gs1_prefix(619) == {:ok, "GS1 Tunisia"} 266 | assert lookup_gs1_prefix(620) == {:ok, "GS1 Tanzania"} 267 | assert lookup_gs1_prefix(621) == {:ok, "GS1 Syria"} 268 | assert lookup_gs1_prefix(622) == {:ok, "GS1 Egypt"} 269 | assert lookup_gs1_prefix(623) == {:ok, "GS1 Brunei"} 270 | assert lookup_gs1_prefix(624) == {:ok, "GS1 Libya"} 271 | assert lookup_gs1_prefix(625) == {:ok, "GS1 Jordan"} 272 | assert lookup_gs1_prefix(626) == {:ok, "GS1 Iran"} 273 | assert lookup_gs1_prefix(627) == {:ok, "GS1 Kuwait"} 274 | assert lookup_gs1_prefix(628) == {:ok, "GS1 Saudi Arabia"} 275 | assert lookup_gs1_prefix(629) == {:ok, "GS1 Emirates"} 276 | assert lookup_gs1_prefix(640) == {:ok, "GS1 Finland"} 277 | assert lookup_gs1_prefix(690) == {:ok, "GS1 China"} 278 | assert lookup_gs1_prefix(700) == {:ok, "GS1 Norway"} 279 | assert lookup_gs1_prefix(729) == {:ok, "GS1 Israel"} 280 | assert lookup_gs1_prefix(730) == {:ok, "GS1 Sweden"} 281 | assert lookup_gs1_prefix(740) == {:ok, "GS1 Guatemala"} 282 | assert lookup_gs1_prefix(741) == {:ok, "GS1 El Salvador"} 283 | assert lookup_gs1_prefix(742) == {:ok, "GS1 Honduras"} 284 | assert lookup_gs1_prefix(743) == {:ok, "GS1 Nicaragua"} 285 | assert lookup_gs1_prefix(744) == {:ok, "GS1 Costa Rica"} 286 | assert lookup_gs1_prefix(745) == {:ok, "GS1 Panama"} 287 | assert lookup_gs1_prefix(746) == {:ok, "GS1 Republica Dominicana"} 288 | assert lookup_gs1_prefix(750) == {:ok, "GS1 Mexico"} 289 | assert lookup_gs1_prefix(754) == {:ok, "GS1 Canada"} 290 | assert lookup_gs1_prefix(759) == {:ok, "GS1 Venezuela"} 291 | assert lookup_gs1_prefix(760) == {:ok, "GS1 Schweiz, Suisse, Svizzera"} 292 | assert lookup_gs1_prefix(770) == {:ok, "GS1 Colombia"} 293 | assert lookup_gs1_prefix(773) == {:ok, "GS1 Uruguay"} 294 | assert lookup_gs1_prefix(775) == {:ok, "GS1 Peru"} 295 | assert lookup_gs1_prefix(777) == {:ok, "GS1 Bolivia"} 296 | assert lookup_gs1_prefix(778) == {:ok, "GS1 Argentina"} 297 | assert lookup_gs1_prefix(780) == {:ok, "GS1 Chile"} 298 | assert lookup_gs1_prefix(784) == {:ok, "GS1 Paraguay"} 299 | assert lookup_gs1_prefix(786) == {:ok, "GS1 Ecuador"} 300 | assert lookup_gs1_prefix(789) == {:ok, "GS1 Brasil"} 301 | assert lookup_gs1_prefix(800) == {:ok, "GS1 Italy"} 302 | assert lookup_gs1_prefix(840) == {:ok, "GS1 Spain"} 303 | assert lookup_gs1_prefix(850) == {:ok, "GS1 Cuba"} 304 | assert lookup_gs1_prefix(858) == {:ok, "GS1 Slovakia"} 305 | assert lookup_gs1_prefix(859) == {:ok, "GS1 Czech"} 306 | assert lookup_gs1_prefix(860) == {:ok, "GS1 Serbia"} 307 | assert lookup_gs1_prefix(865) == {:ok, "GS1 Mongolia"} 308 | assert lookup_gs1_prefix(867) == {:ok, "GS1 North Korea"} 309 | assert lookup_gs1_prefix(868) == {:ok, "GS1 Turkey"} 310 | assert lookup_gs1_prefix(870) == {:ok, "GS1 Netherlands"} 311 | assert lookup_gs1_prefix(880) == {:ok, "GS1 South Korea"} 312 | assert lookup_gs1_prefix(884) == {:ok, "GS1 Cambodia"} 313 | assert lookup_gs1_prefix(885) == {:ok, "GS1 Thailand"} 314 | assert lookup_gs1_prefix(888) == {:ok, "GS1 Singapore"} 315 | assert lookup_gs1_prefix(890) == {:ok, "GS1 India"} 316 | assert lookup_gs1_prefix(893) == {:ok, "GS1 Vietnam"} 317 | assert lookup_gs1_prefix(896) == {:ok, "GS1 Pakistan"} 318 | assert lookup_gs1_prefix(899) == {:ok, "GS1 Indonesia"} 319 | assert lookup_gs1_prefix(900) == {:ok, "GS1 Austria"} 320 | assert lookup_gs1_prefix(930) == {:ok, "GS1 Australia"} 321 | assert lookup_gs1_prefix(940) == {:ok, "GS1 New Zealand"} 322 | assert lookup_gs1_prefix(950) == {:ok, "GS1 Global Office"} 323 | 324 | assert lookup_gs1_prefix(951) == 325 | {:ok, 326 | "Used to issue General Manager Numbers for the EPC General Identifier (GID) scheme as defined by the EPC Tag Data Standard*"} 327 | 328 | assert lookup_gs1_prefix(955) == {:ok, "GS1 Malaysia"} 329 | assert lookup_gs1_prefix(958) == {:ok, "GS1 Macau"} 330 | assert lookup_gs1_prefix(960) == {:ok, "Global Office (GTIN-8s)*"} 331 | assert lookup_gs1_prefix(977) == {:ok, "Serial publications (ISSN)"} 332 | assert lookup_gs1_prefix(978) == {:ok, "Bookland (ISBN)"} 333 | assert lookup_gs1_prefix(980) == {:ok, "Refund receipts"} 334 | assert lookup_gs1_prefix(981) == {:ok, "GS1 coupon identification for common currency areas"} 335 | assert lookup_gs1_prefix(990) == {:ok, "GS1 coupon identification"} 336 | end 337 | 338 | describe "integration tests" do 339 | test "generate then validate GTIN-8" do 340 | base = "1234567" 341 | {:ok, generated} = generate_gtin_code(base) 342 | assert {:ok, "GTIN-8"} == gtin_check_digit(generated) 343 | end 344 | 345 | test "generate then validate GTIN-12" do 346 | base = "12345678901" 347 | {:ok, generated} = generate_gtin_code(base) 348 | assert {:ok, "GTIN-12"} == gtin_check_digit(generated) 349 | end 350 | 351 | test "generate then validate GTIN-13" do 352 | base = "123456789012" 353 | {:ok, generated} = generate_gtin_code(base) 354 | assert {:ok, "GTIN-13"} == gtin_check_digit(generated) 355 | end 356 | 357 | test "generate then validate GTIN-14" do 358 | base = "1234567890123" 359 | {:ok, generated} = generate_gtin_code(base) 360 | assert {:ok, "GTIN-14"} == gtin_check_digit(generated) 361 | end 362 | 363 | test "convert GTIN-8 to GTIN-13" do 364 | gtin8 = "12345678" 365 | # Convert to GTIN-13 by adding 5 leading zeros 366 | gtin13 = "00000" <> gtin8 367 | # Recalculate check digit 368 | {:ok, converted} = generate_gtin_code(String.slice(gtin13, 0..11)) 369 | assert {:ok, "GTIN-13"} == gtin_check_digit(converted) 370 | end 371 | 372 | test "convert GTIN-13 to GTIN-14" do 373 | gtin13 = "1234567890123" 374 | # Convert to GTIN-14 by adding leading 1 375 | gtin14 = "1" <> String.slice(gtin13, 0..11) 376 | # Recalculate check digit 377 | {:ok, converted} = generate_gtin_code(String.slice(gtin14, 0..12)) 378 | assert {:ok, "GTIN-14"} == gtin_check_digit(converted) 379 | end 380 | 381 | test "full conversion: GTIN-8 to GTIN-14" do 382 | gtin8 = "12345678" 383 | # First convert to GTIN-13 384 | gtin13 = "00000" <> gtin8 385 | {:ok, gtin13_with_check} = generate_gtin_code(String.slice(gtin13, 0..11)) 386 | 387 | # Then convert to GTIN-14 388 | gtin14 = "1" <> String.slice(gtin13_with_check, 0..11) 389 | {:ok, gtin14_with_check} = generate_gtin_code(String.slice(gtin14, 0..12)) 390 | 391 | assert {:ok, "GTIN-14"} == gtin_check_digit(gtin14_with_check) 392 | end 393 | 394 | test "find_gs1_prefix_country after validation" do 395 | gtin = "6291041500213" 396 | {:ok, _} = gtin_check_digit(gtin) 397 | assert {:ok, "GS1 Emirates"} == find_gs1_prefix_country(gtin) 398 | end 399 | end 400 | 401 | describe "normalize function tests" do 402 | test "converts GTIN-8 to GTIN-14 correctly" do 403 | gtin8 = "12345670" 404 | {:ok, normalized} = ExGtin.normalize(gtin8) 405 | assert normalized == "10000012345677" 406 | assert {:ok, "GTIN-14"} == gtin_check_digit(normalized) 407 | end 408 | 409 | test "converts GTIN-12 to GTIN-14 correctly" do 410 | gtin12 = "123456789012" 411 | {:ok, normalized} = ExGtin.normalize(gtin12) 412 | assert normalized == "10123456789019" 413 | assert {:ok, "GTIN-14"} == gtin_check_digit(normalized) 414 | end 415 | 416 | test "converts GTIN-13 to GTIN-14 correctly" do 417 | gtin13 = "1234567890128" 418 | {:ok, normalized} = ExGtin.normalize(gtin13) 419 | assert normalized == "11234567890125" 420 | assert {:ok, "GTIN-14"} == gtin_check_digit(normalized) 421 | end 422 | 423 | test "leaves GTIN-14 unchanged" do 424 | gtin14 = "12345678901231" 425 | {:ok, normalized} = ExGtin.normalize(gtin14) 426 | assert normalized == gtin14 427 | assert {:ok, "GTIN-14"} == gtin_check_digit(normalized) 428 | end 429 | 430 | test "handles ISBN-10 conversion correctly" do 431 | isbn = "0205080057" 432 | {:ok, normalized} = ExGtin.normalize(isbn) 433 | assert normalized == "09780205080052" 434 | assert {:ok, "GTIN-14"} == gtin_check_digit(normalized) 435 | end 436 | 437 | test "rejects invalid GTIN-8" do 438 | # Invalid check digit 439 | invalid_gtin8 = "12345679" 440 | assert {:error, "Invalid Code"} == ExGtin.normalize(invalid_gtin8) 441 | end 442 | 443 | test "rejects invalid GTIN-12" do 444 | # Invalid check digit 445 | invalid_gtin12 = "123456789013" 446 | assert {:error, "Invalid Code"} == ExGtin.normalize(invalid_gtin12) 447 | end 448 | 449 | test "rejects invalid GTIN-13" do 450 | # Invalid check digit 451 | invalid_gtin13 = "1234567890124" 452 | assert {:error, "Invalid Code"} == ExGtin.normalize(invalid_gtin13) 453 | end 454 | 455 | test "rejects invalid GTIN-14" do 456 | # Invalid check digit 457 | invalid_gtin14 = "12345678901235" 458 | assert {:error, "Invalid Code"} == ExGtin.normalize(invalid_gtin14) 459 | end 460 | end 461 | 462 | describe "edge cases for gtin_check_digit" do 463 | test "with whitespace" do 464 | assert_raise ArgumentError, fn -> 465 | gtin_check_digit(" 6291041500213 ") 466 | end 467 | 468 | assert_raise ArgumentError, fn -> 469 | gtin_check_digit("\t6291041500213\t") 470 | end 471 | end 472 | 473 | test "with non-numeric characters" do 474 | assert_raise ArgumentError, fn -> 475 | gtin_check_digit("629104150021a") 476 | end 477 | 478 | assert_raise ArgumentError, fn -> 479 | gtin_check_digit("629104150021!") 480 | end 481 | end 482 | 483 | test "with empty string" do 484 | assert {:error, "Invalid GTIN Code Length"} == gtin_check_digit("") 485 | end 486 | 487 | test "with very large numbers" do 488 | # Test with a number that's too large for standard integer representation 489 | assert {:error, "Invalid GTIN Code Length"} == 490 | gtin_check_digit("999999999999999999999999999999") 491 | end 492 | 493 | test "with negative numbers" do 494 | assert_raise ArgumentError, fn -> 495 | gtin_check_digit("-6291041500213") 496 | end 497 | end 498 | 499 | test "with decimal numbers" do 500 | assert_raise ArgumentError, fn -> 501 | gtin_check_digit("629104150021.3") 502 | end 503 | end 504 | end 505 | 506 | describe "edge cases for find_gs1_prefix_country" do 507 | test "with special prefixes in different formats" do 508 | # ISBN-10 converted to GTIN-13 509 | assert {:ok, "Bookland (ISBN)"} == find_gs1_prefix_country("9780205080052") 510 | # ISSN converted to GTIN-13 511 | assert {:ok, "Serial publications (ISSN)"} == find_gs1_prefix_country("9771234567003") 512 | end 513 | end 514 | 515 | describe "edge cases for normalize" do 516 | test "with ISBN-13 codes" do 517 | isbn13 = "9780205080052" 518 | assert {:ok, "19780205080059"} == ExGtin.normalize(isbn13) 519 | end 520 | 521 | test "with ISSN codes" do 522 | issn = "9771234567003" 523 | assert {:ok, "19771234567000"} == ExGtin.normalize(issn) 524 | end 525 | 526 | test "with UPC-A codes" do 527 | upc = "012345678905" 528 | assert {:ok, "10012345678902"} == ExGtin.normalize(upc) 529 | end 530 | 531 | test "with mixed format inputs" do 532 | # GTIN-8 with ISBN-10 format 533 | # Valid GTIN-8 534 | mixed = "40170725" 535 | assert {:ok, "10000040170722"} == ExGtin.normalize(mixed) 536 | end 537 | end 538 | end 539 | --------------------------------------------------------------------------------