├── .circleci ├── .npmignore └── config.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── src ├── .npmignore ├── gs1 ├── applicationIdentifiers.js ├── applicationIdentifiers.spec.js ├── dataReaders.js ├── dataReaders.spec.js ├── parser.js ├── parser.spec.js ├── symbologies.js ├── symbologies.spec.js └── utils │ ├── injectDecimal.js │ └── injectDecimal.spec.js ├── index.js └── index.spec.js /.circleci/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build_v10: &build 4 | docker: 5 | - image: circleci/node:10.10.0 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | 10 | - restore_cache: 11 | keys: 12 | - v1-dependencies-{{ checksum "package.json" }} 13 | # fallback to using the latest cache if no exact match is found 14 | - v1-dependencies- 15 | 16 | - run: npm ci 17 | 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-{{ checksum "package.json" }} 22 | 23 | - run: npm test 24 | 25 | build_v12: 26 | <<: *build 27 | docker: 28 | - image: circleci/node:12 29 | 30 | build_v14: 31 | <<: *build 32 | docker: 33 | - image: circleci/node:14 34 | 35 | workflows: 36 | version: 2 37 | build_and_test: 38 | jobs: 39 | - build_v10 40 | - build_v12 41 | - build_v14 42 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "comma-dangle": ["error", "always-multiline"], 5 | "no-trailing-spaces": "error" 6 | }, 7 | "env": { 8 | "jest": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,linux,macos,windows 2 | # Edit at https://www.gitignore.io/?templates=node,linux,macos,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### Node ### 48 | # Logs 49 | logs 50 | *.log 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | *.pid.lock 60 | 61 | # Directory for instrumented libs generated by jscoverage/JSCover 62 | lib-cov 63 | 64 | # Coverage directory used by tools like istanbul 65 | coverage 66 | 67 | # nyc test coverage 68 | .nyc_output 69 | 70 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 71 | .grunt 72 | 73 | # Bower dependency directory (https://bower.io/) 74 | bower_components 75 | 76 | # node-waf configuration 77 | .lock-wscript 78 | 79 | # Compiled binary addons (https://nodejs.org/api/addons.html) 80 | build/Release 81 | 82 | # Dependency directories 83 | node_modules/ 84 | jspm_packages/ 85 | 86 | # TypeScript v1 declaration files 87 | typings/ 88 | 89 | # Optional npm cache directory 90 | .npm 91 | 92 | # Optional eslint cache 93 | .eslintcache 94 | 95 | # Optional REPL history 96 | .node_repl_history 97 | 98 | # Output of 'npm pack' 99 | *.tgz 100 | 101 | # Yarn Integrity file 102 | .yarn-integrity 103 | 104 | # dotenv environment variables file 105 | .env 106 | .env.test 107 | 108 | # parcel-bundler cache (https://parceljs.org/) 109 | .cache 110 | 111 | # next.js build output 112 | .next 113 | 114 | # nuxt.js build output 115 | .nuxt 116 | 117 | # vuepress build output 118 | .vuepress/dist 119 | 120 | # Serverless directories 121 | .serverless/ 122 | 123 | # FuseBox cache 124 | .fusebox/ 125 | 126 | # DynamoDB Local files 127 | .dynamodb/ 128 | 129 | ### Windows ### 130 | # Windows thumbnail cache files 131 | Thumbs.db 132 | ehthumbs.db 133 | ehthumbs_vista.db 134 | 135 | # Dump file 136 | *.stackdump 137 | 138 | # Folder config file 139 | [Dd]esktop.ini 140 | 141 | # Recycle Bin used on file shares 142 | $RECYCLE.BIN/ 143 | 144 | # Windows Installer files 145 | *.cab 146 | *.msi 147 | *.msix 148 | *.msm 149 | *.msp 150 | 151 | # Windows shortcuts 152 | *.lnk 153 | 154 | # End of https://www.gitignore.io/api/node,linux,macos,windows 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## LICENSE 2 | 3 | (MIT License) 4 | 5 | Copyright (c) 2019 Joakim Hedlund 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 | # Bark JS 2 | 3 | [ ![npm version](https://img.shields.io/npm/v/bark-js.svg?style=flat) ](https://npmjs.org/package/bark-js "View this project on npm") [ ![Travis](https://img.shields.io/travis/Sleavely/Bark-JS) ](https://travis-ci.org/Sleavely/Bark-JS) [ ![Issues](https://img.shields.io/github/issues/Sleavely/Bark-JS.svg) ](https://github.com/Sleavely/Bark-JS/issues) 4 | 5 | Bark parses GS1-128 barcodes and extracts the catalogued data according to the [GS1 General Specifications (PDF)](https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf). It can also parse other SKU-related formats to convert into GTINs in GS1, such as EAN-13, ITF-14 and UPC-A. 6 | 7 | ## How to use it 8 | 9 | ``` 10 | npm install bark-js 11 | ``` 12 | 13 | ### Examples 14 | 15 | Let's pretend we scan [the box in this photo](https://goo.gl/photos/HCE7WrNHDKvQL5ei8). 16 | 17 | ```javascript 18 | const bark = require('bark-js') 19 | 20 | bark( '015730033004265615171019' ) 21 | // returns: 22 | { 23 | symbology: 'unknown', 24 | elements: [ 25 | { 26 | ai: '01', 27 | title: 'GTIN', 28 | value: '57300330042656', 29 | raw: '57300330042656' 30 | }, 31 | { 32 | ai: '15', 33 | title: 'BEST BEFORE or BEST BY', 34 | value: '2017-10-19', 35 | raw: '171019' 36 | } 37 | ], 38 | originalBarcode: '015730033004265615171019' 39 | } 40 | ``` 41 | 42 | If you are going to scan simple barcodes (e.g. UPC-A, EAN-13, ITF-14, etc.) you can set the `assumeGtin` option to treat shorter barcodes (11-14 digits) as GS1-128 with a GTIN AI: 43 | 44 | ```javascript 45 | const bark = require('bark-js') 46 | 47 | bark( '09002490100094', { assumeGtin: true } ) 48 | // returns: 49 | { 50 | symbology: 'unknown', 51 | elements: [ 52 | { 53 | ai: '01', 54 | title: 'GTIN', 55 | value: '09002490100094', 56 | raw: '09002490100094' 57 | } 58 | ], 59 | originalBarcode: '0109002490100094' 60 | } 61 | ``` 62 | 63 | Depending on your barcode reader, you may receive FNC characters that arent the `` (ASCII 29) character. To set the group separator yourself, pass the `fnc` option: 64 | 65 | ```javascript 66 | const bark = require('bark-js') 67 | 68 | bark( '10FRIDGEX0109002490100094', { fnc: 'X' } ) 69 | // returns: 70 | { 71 | symbology: 'unknown', 72 | elements: [ 73 | { 74 | ai: '10', 75 | title: 'BATCH/LOT', 76 | value: 'FRIDGE', 77 | raw: 'FRIDGEX' 78 | }, 79 | { 80 | ai: '01', 81 | title: 'GTIN', 82 | value: '09002490100094', 83 | raw: '09002490100094' 84 | } 85 | ], 86 | originalBarcode: '10FRIDGEX0109002490100094' 87 | } 88 | ``` 89 | 90 | Depending on the type of elements in your code, the parsers may append additional fields to such as `isoCurrencyCode` and `amount` for your convenience: 91 | 92 | ```javascript 93 | const bark = require('bark-js') 94 | 95 | bark( '393297817999' ) 96 | // returns: 97 | { 98 | symbology: 'unknown', 99 | elements: [ 100 | { 101 | ai: '3932', 102 | title: 'PRICE', 103 | value: '978179.99', 104 | isoCurrencyCode: '978', 105 | amount: '179.99', 106 | raw: '97817999' 107 | } 108 | ], 109 | originalBarcode: '393297817999' 110 | } 111 | ``` 112 | 113 | ## Contributing 114 | 115 | Pull requests to Sleavely/Bark-JS are encouraged and appreciated! 116 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = require('./src/index') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bark-js", 3 | "version": "1.2.1", 4 | "description": "Parse barcode inputs into a unified GS1-128 like format", 5 | "homepage": "https://github.com/Sleavely/Bark-js", 6 | "repository": "github:Sleavely/Bark-JS", 7 | "author": "Joakim Hedlund ", 8 | "keywords": [ 9 | "barcode", 10 | "QR", 11 | "parser", 12 | "EAN", 13 | "UPC", 14 | "GS1" 15 | ], 16 | "main": "index.js", 17 | "scripts": { 18 | "test": "jest --verbose --silent && eslint ." 19 | }, 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@types/jest": "^24.9.1", 23 | "eslint": "^5.16.0", 24 | "eslint-config-standard": "^12.0.0", 25 | "eslint-plugin-import": "^2.26.0", 26 | "eslint-plugin-node": "^9.2.0", 27 | "eslint-plugin-promise": "^4.3.1", 28 | "eslint-plugin-standard": "^4.1.0", 29 | "jest": "^24.9.0", 30 | "jest-extended": "^0.11.5" 31 | }, 32 | "jest": { 33 | "testEnvironment": "node", 34 | "setupFilesAfterEnv": [ 35 | "jest-extended" 36 | ] 37 | }, 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /src/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js 2 | -------------------------------------------------------------------------------- /src/gs1/applicationIdentifiers.js: -------------------------------------------------------------------------------- 1 | 2 | const { 3 | fixedLength, 4 | variableLength, 5 | date, 6 | dateTime, 7 | dateRange, 8 | fixedLengthDecimal, 9 | variableLengthDecimal, 10 | variableLengthISOCurrency, 11 | variableLengthISOCountry, 12 | } = require('./dataReaders') 13 | 14 | exports.parseAi = (barcode) => { 15 | switch (barcode.slice(0, 1)) { 16 | case '0': 17 | switch (barcode.slice(1, 2)) { 18 | case '0': 19 | // SSCC (Serial Shipping Container Code) 20 | return { ai: '00', title: 'SSCC', parser: fixedLength(18) } 21 | case '1': 22 | // Global Trade Item Number (GTIN) 23 | return { ai: '01', title: 'GTIN', parser: fixedLength(14) } 24 | case '2': 25 | // GTIN of Contained Trade Items 26 | return { ai: '02', title: 'CONTENT', parser: fixedLength(14) } 27 | } 28 | break 29 | case '1': 30 | switch (barcode.slice(1, 2)) { 31 | case '0': 32 | return { ai: '10', title: 'BATCH/LOT', parser: variableLength(20) } 33 | case '1': 34 | return { ai: '11', title: 'PROD DATE', parser: date() } 35 | case '2': 36 | return { ai: '12', title: 'DUE DATE', parser: date() } 37 | case '3': 38 | return { ai: '13', title: 'PACK DATE', parser: date() } 39 | case '4': 40 | break 41 | case '5': 42 | return { ai: '15', title: 'BEST BEFORE or BEST BY', parser: date() } 43 | case '6': 44 | return { ai: '16', title: 'SELL BY', parser: date() } 45 | case '7': 46 | return { ai: '17', title: 'USE BY OR EXPIRY', parser: date() } 47 | } 48 | break 49 | case '2': 50 | switch (barcode.slice(1, 2)) { 51 | case '0': 52 | return { ai: '20', title: 'VARIANT', parser: fixedLength(2) } 53 | case '1': 54 | return { ai: '21', title: 'SERIAL', parser: variableLength(20) } 55 | case '2': 56 | return { ai: '22', title: 'CPV', parser: variableLength(20) } 57 | case '3': 58 | switch (barcode.slice(2, 3)) { 59 | case '5': 60 | return { ai: '235', title: 'TPX', parser: variableLength(28) } 61 | } 62 | break 63 | case '4': 64 | switch (barcode.slice(2, 3)) { 65 | case '0': 66 | return { ai: '240', title: 'ADDITIONAL ID', parser: variableLength(30) } 67 | case '1': 68 | return { ai: '241', title: 'CUST. PART NO.', parser: variableLength(30) } 69 | case '2': 70 | return { ai: '242', title: 'MTO VARIANT', parser: variableLength(6) } 71 | case '3': 72 | return { ai: '243', title: 'PCN', parser: variableLength(20) } 73 | } 74 | break 75 | case '5': 76 | switch (barcode.slice(2, 3)) { 77 | case '0': 78 | return { ai: '250', title: 'SECONDARYSERIAL', parser: variableLength(30) } 79 | case '1': 80 | return { ai: '251', title: 'REF. TO SOURCE', parser: variableLength(30) } 81 | case '2': 82 | break 83 | case '3': 84 | return { ai: '253', title: 'GDTI', parser: variableLength(30) } 85 | case '4': 86 | return { ai: '254', title: 'GLN EXTENSION COMPONENT', parser: variableLength(20) } 87 | case '5': 88 | return { ai: '255', title: 'GCN', parser: variableLength(25) } 89 | } 90 | break 91 | } 92 | break 93 | case '3': 94 | switch (barcode.slice(1, 2)) { 95 | case '0': 96 | return { ai: '30', title: 'VAR. COUNT', parser: variableLength(8) } 97 | case '1': { 98 | const decIndicator = barcode.slice(3, 4) 99 | // Decimal point offset cant be higher than the values lengths 100 | if (parseInt(decIndicator, 10) > 6) break 101 | switch (barcode.slice(2, 3)) { 102 | case '0': 103 | return { ai: `310${decIndicator}`, title: 'NET WEIGHT (kg)', parser: fixedLengthDecimal(6, decIndicator) } 104 | case '1': 105 | return { ai: `311${decIndicator}`, title: 'LENGTH (m)', parser: fixedLengthDecimal(6, decIndicator) } 106 | case '2': 107 | return { ai: `312${decIndicator}`, title: 'WIDTH (m)', parser: fixedLengthDecimal(6, decIndicator) } 108 | case '3': 109 | return { ai: `313${decIndicator}`, title: 'HEIGHT (m)', parser: fixedLengthDecimal(6, decIndicator) } 110 | case '4': 111 | return { ai: `314${decIndicator}`, title: 'AREA (m^2)', parser: fixedLengthDecimal(6, decIndicator) } 112 | case '5': 113 | return { ai: `315${decIndicator}`, title: 'NET VOLUME (l)', parser: fixedLengthDecimal(6, decIndicator) } 114 | case '6': 115 | return { ai: `316${decIndicator}`, title: 'NET VOLUME (m^3)', parser: fixedLengthDecimal(6, decIndicator) } 116 | } 117 | break 118 | } 119 | case '2': { // 32nn 120 | const titles = [ 121 | 'NET WEIGHT (lb)', 122 | 'LENGTH (in)', 123 | 'LENGTH (ft)', 124 | 'LENGTH (yd)', 125 | 'WIDTH (in)', 126 | 'WIDTH (ft)', 127 | 'WIDTH (yd)', 128 | 'HEIGHT (in)', 129 | 'HEIGHT (ft)', 130 | 'HEIGHT (yd)', 131 | ] 132 | const titleIndicator = parseInt(barcode.slice(2, 3), 10) 133 | const title = titles[titleIndicator] 134 | if (!title) break 135 | 136 | const valueLength = 6 137 | const decIndicator = parseInt(barcode.slice(3, 4), 10) 138 | if (decIndicator > valueLength) break 139 | 140 | return { ai: `${barcode.slice(0, 4)}`, title, parser: fixedLengthDecimal(valueLength, decIndicator) } 141 | } 142 | case '3': { // 33nn 143 | const titles = [ 144 | 'GROSS WEIGHT (kg)', 145 | 'LENGTH (m), log', 146 | 'WIDTH (m), log', 147 | 'HEIGHT (m), log', 148 | 'AREA (m^2), log', 149 | 'VOLUME (l), log', 150 | 'VOLUME (m^3), log', 151 | 'KG PER m^2', 152 | ] 153 | const titleIndicator = parseInt(barcode.slice(2, 3), 10) 154 | const title = titles[titleIndicator] 155 | if (!title) break 156 | 157 | const valueLength = 6 158 | const decIndicator = parseInt(barcode.slice(3, 4), 10) 159 | if (decIndicator > valueLength) break 160 | 161 | return { ai: `${barcode.slice(0, 4)}`, title, parser: fixedLengthDecimal(valueLength, decIndicator) } 162 | } 163 | case '4': { // 34nn 164 | const titles = [ 165 | 'GROSS WEIGHT (lb)', 166 | 'LENGTH (in), log', 167 | 'LENGTH (ft), log', 168 | 'LENGTH (yd), log', 169 | 'WIDTH (in), log', 170 | 'WIDTH (ft), log', 171 | 'WIDTH (yd), log', 172 | 'HEIGHT (in), log', 173 | 'HEIGHT (ft), log', 174 | 'HEIGHT (yd), log', 175 | ] 176 | const titleIndicator = parseInt(barcode.slice(2, 3), 10) 177 | const title = titles[titleIndicator] 178 | if (!title) break 179 | 180 | const valueLength = 6 181 | const decIndicator = parseInt(barcode.slice(3, 4), 10) 182 | if (decIndicator > valueLength) break 183 | 184 | return { ai: `${barcode.slice(0, 4)}`, title, parser: fixedLengthDecimal(valueLength, decIndicator) } 185 | } 186 | case '5': { // 35nn 187 | const titles = [ 188 | 'AREA (in^2)', 189 | 'AREA (ft^2)', 190 | 'AREA (yd^2)', 191 | 'AREA (in^2), log', 192 | 'AREA (ft^2), log', 193 | 'AREA (yd^2), log', 194 | 'NET WEIGHT (t oz)', 195 | 'NET VOLUME (oz)', 196 | ] 197 | const titleIndicator = parseInt(barcode.slice(2, 3), 10) 198 | const title = titles[titleIndicator] 199 | if (!title) break 200 | 201 | const valueLength = 6 202 | const decIndicator = parseInt(barcode.slice(3, 4), 10) 203 | if (decIndicator > valueLength) break 204 | 205 | return { ai: `${barcode.slice(0, 4)}`, title, parser: fixedLengthDecimal(valueLength, decIndicator) } 206 | } 207 | case '6': { // 36nn 208 | const titles = [ 209 | 'NET VOLUME (qt)', 210 | 'NET VOLUME (gal)', 211 | 'VOLUME (qt), log', 212 | 'VOLUME (gal), log', 213 | 'VOLUME (in^3)', 214 | 'VOLUME (ft^3)', 215 | 'VOLUME (yd^3)', 216 | 'VOLUME (in^3), log', 217 | 'VOLUME (ft^3), log', 218 | 'VOLUME (yd^3), log', 219 | ] 220 | const titleIndicator = parseInt(barcode.slice(2, 3), 10) 221 | const title = titles[titleIndicator] 222 | if (!title) break 223 | 224 | const valueLength = 6 225 | const decIndicator = parseInt(barcode.slice(3, 4), 10) 226 | if (decIndicator > valueLength) break 227 | 228 | return { ai: `${barcode.slice(0, 4)}`, title, parser: fixedLengthDecimal(valueLength, decIndicator) } 229 | } 230 | case '7': 231 | return { ai: `37`, title: 'COUNT', parser: variableLength(8) } 232 | case '9': 233 | const decIndicator = parseInt(barcode.slice(3, 4), 10) 234 | switch (barcode.slice(2, 3)) { 235 | case '0': 236 | return { ai: `390${decIndicator}`, title: 'AMOUNT', parser: variableLengthDecimal(15, decIndicator) } 237 | case '1': 238 | return { ai: `391${decIndicator}`, title: 'AMOUNT', parser: variableLengthISOCurrency(18, decIndicator) } 239 | case '2': 240 | return { ai: `392${decIndicator}`, title: 'PRICE', parser: variableLengthDecimal(15, decIndicator) } 241 | case '3': 242 | return { ai: `393${decIndicator}`, title: 'PRICE', parser: variableLengthISOCurrency(18, decIndicator) } 243 | case '4': 244 | if (decIndicator > 4) break 245 | return { ai: `394${decIndicator}`, title: 'PRCNT OFF', parser: fixedLengthDecimal(4, decIndicator) } 246 | } 247 | break 248 | } 249 | break 250 | case '4': 251 | switch (barcode.slice(1, 3)) { 252 | case '00': 253 | return { ai: '400', title: 'ORDER NUMBER', parser: variableLength(30) } 254 | case '01': 255 | return { ai: '401', title: 'GINC', parser: variableLength(30) } 256 | case '02': 257 | return { ai: '402', title: 'GSIN', parser: variableLength(17) } 258 | case '03': 259 | return { ai: '403', title: 'ROUTE', parser: variableLength(30) } 260 | case '10': 261 | return { ai: '410', title: 'SHIP TO LOC', parser: fixedLength(13) } 262 | case '11': 263 | return { ai: '411', title: 'BILL TO', parser: fixedLength(13) } 264 | case '12': 265 | return { ai: '412', title: 'PURCHASE FROM', parser: fixedLength(13) } 266 | case '13': 267 | return { ai: '413', title: 'SHIP FOR LOC', parser: fixedLength(13) } 268 | case '14': 269 | return { ai: '414', title: 'LOC No', parser: fixedLength(13) } 270 | case '15': 271 | return { ai: '415', title: 'PAY TO', parser: fixedLength(13) } 272 | case '16': 273 | return { ai: '416', title: 'PROD/SERV LOC', parser: fixedLength(13) } 274 | case '17': 275 | return { ai: '417', title: 'PARTY', parser: fixedLength(13) } 276 | case '20': 277 | return { ai: '420', title: 'SHIP TO POST', parser: variableLength(20) } 278 | case '21': 279 | return { ai: '421', title: 'SHIP TO POST', parser: variableLengthISOCountry(20) } 280 | case '22': 281 | return { ai: '422', title: 'ORIGIN', parser: fixedLength(3) } 282 | case '23': 283 | return { ai: '423', title: 'COUNTRY - INITIAL PROCESS.', parser: variableLength(15) } 284 | case '24': 285 | return { ai: '424', title: 'COUNTRY - PROCESS.', parser: fixedLength(3) } 286 | case '25': 287 | return { ai: '425', title: 'COUNTRY - DISASSEMBLY', parser: variableLength(15) } 288 | case '26': 289 | return { ai: '426', title: 'COUNTRY - FULL PROCESS', parser: fixedLength(3) } 290 | case '27': 291 | return { ai: '427', title: 'ORIGIN SUBDIVISION', parser: variableLength(3) } 292 | } 293 | break 294 | case '7': 295 | switch (barcode.slice(1, 3)) { 296 | case '00': 297 | switch (barcode.slice(3, 4)) { 298 | case '1': 299 | return { ai: '7001', title: 'NSN', parser: fixedLength(13) } 300 | case '2': 301 | return { ai: '7002', title: 'MEAT CUT', parser: variableLength(30) } 302 | case '3': 303 | return { ai: '7003', title: 'EXPIRY TIME', parser: dateTime() } 304 | case '4': 305 | return { ai: '7004', title: 'ACTIVE POTENCY', parser: variableLength(4) } 306 | case '5': 307 | return { ai: '7005', title: 'CATCH AREA', parser: variableLength(12) } 308 | case '6': 309 | return { ai: '7006', title: 'FIRST FREEZE DATE', parser: date() } 310 | case '7': 311 | return { ai: '7007', title: 'HARVEST DATE', parser: dateRange() } 312 | case '8': 313 | return { ai: '7008', title: 'AQUATIC SPECIES', parser: variableLength(3) } 314 | case '9': 315 | return { ai: '7009', title: 'FISHING GEAR TYPE', parser: variableLength(10) } 316 | } 317 | break 318 | case '01': 319 | return { ai: '7010', 320 | title: 'PROD METHOD', 321 | parser: (opts) => (output => { 322 | const humanMapping = { 323 | '01': 'Caught at Sea', 324 | '02': 'Caught in Fresh Water', 325 | '03': 'Farmed', 326 | '04': 'Cultivated', 327 | } 328 | return { ...output, human: humanMapping[output.value] } 329 | })(variableLength(2)(opts)) } 330 | 331 | case '02': 332 | switch (barcode.slice(3, 4)) { 333 | case '0': 334 | return { ai: '7020', title: 'REFURB LOT', parser: variableLength(20) } 335 | case '1': 336 | return { ai: '7021', title: 'FUNC STAT', parser: variableLength(20) } 337 | case '2': 338 | return { ai: '7022', title: 'REV STAT', parser: variableLength(20) } 339 | case '3': 340 | return { ai: '7023', title: 'GIAI - ASSEMBLY', parser: variableLength(30) } 341 | } 342 | break 343 | 344 | case '03': 345 | const processor = barcode.slice(3, 4) 346 | return { ai: barcode.slice(0, 4), title: `PROCESSOR # ${processor}`, parser: variableLengthISOCountry(33) } 347 | 348 | case '04': 349 | switch (barcode.slice(3, 4)) { 350 | case '0': 351 | return { ai: '7040', title: 'UIC+EXT', parser: variableLength(4) } 352 | } 353 | break 354 | 355 | case '10': 356 | return { ai: '710', title: 'NHRN PZN', parser: variableLength(20) } 357 | case '11': 358 | return { ai: '711', title: 'NHRN CIP', parser: variableLength(20) } 359 | case '12': 360 | return { ai: '712', title: 'NHRN CN', parser: variableLength(20) } 361 | case '13': 362 | return { ai: '713', title: 'NHRN DRN', parser: variableLength(20) } 363 | case '14': 364 | return { ai: '714', title: 'NHRN AIM', parser: variableLength(20) } 365 | 366 | case '23': 367 | const certReference = barcode.slice(3, 4) 368 | return { ai: barcode.slice(0, 4), title: `CERT # ${certReference}`, parser: variableLength(30) } 369 | 370 | case '24': 371 | switch (barcode.slice(3, 4)) { 372 | case '0': 373 | return { ai: '7240', title: 'PROTOCOL', parser: variableLength(20) } 374 | } 375 | break 376 | } 377 | break 378 | case '8': 379 | switch (barcode.slice(1, 4)) { 380 | case '001': 381 | return { ai: '8001', title: 'DIMENSIONS', parser: variableLength(14) } 382 | case '002': 383 | return { ai: '8002', title: 'CMT No', parser: variableLength(14) } 384 | case '003': 385 | return { ai: '8003', title: 'GRAI', parser: variableLength(30) } 386 | case '004': 387 | return { ai: '8004', title: 'GIAI', parser: variableLength(30) } 388 | case '005': 389 | return { ai: '8005', title: 'PRICE PER UNIT', parser: variableLength(6) } 390 | case '006': 391 | return { ai: '8006', title: 'ITIP', parser: variableLength(18) } 392 | case '007': 393 | return { ai: '8007', title: 'IBAN', parser: variableLength(34) } 394 | case '008': 395 | return { ai: '8008', title: 'PROD TIME', parser: dateTime({ optionalMinutesAndSeconds: true }) } 396 | case '009': 397 | return { ai: '8009', title: 'OPTSEN', parser: variableLength(50) } 398 | case '010': 399 | return { ai: '8010', title: 'CPID', parser: variableLength(30) } 400 | case '011': 401 | return { ai: '8011', title: 'CPID SERIAL', parser: variableLength(12) } 402 | case '012': 403 | return { ai: '8012', title: 'VERSION', parser: variableLength(20) } 404 | case '013': 405 | return { ai: '8013', title: 'GMN', parser: variableLength(30) } 406 | case '017': 407 | return { ai: '8017', title: 'GSRN - PROVIDER', parser: variableLength(18) } 408 | case '018': 409 | return { ai: '8018', title: 'GSRN - RECIPIENT', parser: variableLength(18) } 410 | case '019': 411 | return { ai: '8019', title: 'SRIN', parser: variableLength(10) } 412 | case '020': 413 | return { ai: '8020', title: 'REF No', parser: variableLength(25) } 414 | case '026': 415 | return { ai: '8026', title: 'ITIP CONTENT', parser: variableLength(18) } 416 | case '110': 417 | return { ai: '8110', title: '', parser: variableLength(70) } 418 | case '111': 419 | return { ai: '8111', title: 'POINTS', parser: variableLength(4) } 420 | case '112': 421 | return { ai: '8112', title: '', parser: variableLength(70) } 422 | case '200': 423 | return { ai: '8200', title: 'PRODUCT URL', parser: variableLength(70) } 424 | } 425 | break 426 | case '9': 427 | switch (barcode.slice(1, 2)) { 428 | case '0': 429 | return { ai: '90', title: 'INTERNAL', parser: variableLength(30) } 430 | case '1': 431 | case '2': 432 | case '3': 433 | case '4': 434 | case '5': 435 | case '6': 436 | case '7': 437 | case '8': 438 | case '9': 439 | return { ai: barcode.slice(0, 2), title: 'INTERNAL', parser: variableLength(90) } 440 | } 441 | break 442 | } 443 | throw new Error(`Invalid AI at start of barcode section: ${barcode}. This likely occurred because your barcode has a value that is longer than the permitted max length for a particular AI.`) 444 | } 445 | -------------------------------------------------------------------------------- /src/gs1/applicationIdentifiers.spec.js: -------------------------------------------------------------------------------- 1 | 2 | const { parseAi } = require('./applicationIdentifiers') 3 | 4 | it.each([ 5 | ['00', 'SSCC'], 6 | ['01', 'GTIN'], 7 | ['02', 'CONTENT'], 8 | ['10', 'BATCH/LOT'], 9 | ['11', 'PROD DATE'], 10 | ['12', 'DUE DATE'], 11 | ['13', 'PACK DATE'], 12 | ['15', 'BEST BEFORE or BEST BY'], 13 | ['16', 'SELL BY'], 14 | ['17', 'USE BY OR EXPIRY'], 15 | ['20', 'VARIANT'], 16 | ['21', 'SERIAL'], 17 | ['22', 'CPV'], 18 | ['240', 'ADDITIONAL ID'], 19 | ['241', 'CUST. PART NO.'], 20 | ['242', 'MTO VARIANT'], 21 | ['243', 'PCN'], 22 | ['250', 'SECONDARYSERIAL'], 23 | ['251', 'REF. TO SOURCE'], 24 | ['253', 'GDTI'], 25 | ['254', 'GLN EXTENSION COMPONENT'], 26 | ['255', 'GCN'], 27 | ['30', 'VAR. COUNT'], 28 | ['3100', 'NET WEIGHT (kg)'], 29 | ['3101', 'NET WEIGHT (kg)'], 30 | ['3102', 'NET WEIGHT (kg)'], 31 | ['3103', 'NET WEIGHT (kg)'], 32 | ['3104', 'NET WEIGHT (kg)'], 33 | ['3105', 'NET WEIGHT (kg)'], 34 | ['3110', 'LENGTH (m)'], 35 | ['3111', 'LENGTH (m)'], 36 | ['3112', 'LENGTH (m)'], 37 | ['3113', 'LENGTH (m)'], 38 | ['3114', 'LENGTH (m)'], 39 | ['3115', 'LENGTH (m)'], 40 | ['3120', 'WIDTH (m)'], 41 | ['3121', 'WIDTH (m)'], 42 | ['3122', 'WIDTH (m)'], 43 | ['3123', 'WIDTH (m)'], 44 | ['3124', 'WIDTH (m)'], 45 | ['3125', 'WIDTH (m)'], 46 | ['3130', 'HEIGHT (m)'], 47 | ['3131', 'HEIGHT (m)'], 48 | ['3132', 'HEIGHT (m)'], 49 | ['3133', 'HEIGHT (m)'], 50 | ['3134', 'HEIGHT (m)'], 51 | ['3135', 'HEIGHT (m)'], 52 | ['3140', 'AREA (m^2)'], 53 | ['3141', 'AREA (m^2)'], 54 | ['3142', 'AREA (m^2)'], 55 | ['3143', 'AREA (m^2)'], 56 | ['3144', 'AREA (m^2)'], 57 | ['3145', 'AREA (m^2)'], 58 | ['3150', 'NET VOLUME (l)'], 59 | ['3151', 'NET VOLUME (l)'], 60 | ['3152', 'NET VOLUME (l)'], 61 | ['3153', 'NET VOLUME (l)'], 62 | ['3154', 'NET VOLUME (l)'], 63 | ['3155', 'NET VOLUME (l)'], 64 | ['3160', 'NET VOLUME (m^3)'], 65 | ['3161', 'NET VOLUME (m^3)'], 66 | ['3162', 'NET VOLUME (m^3)'], 67 | ['3163', 'NET VOLUME (m^3)'], 68 | ['3164', 'NET VOLUME (m^3)'], 69 | ['3165', 'NET VOLUME (m^3)'], 70 | ['3200', 'NET WEIGHT (lb)'], 71 | ['3201', 'NET WEIGHT (lb)'], 72 | ['3202', 'NET WEIGHT (lb)'], 73 | ['3203', 'NET WEIGHT (lb)'], 74 | ['3204', 'NET WEIGHT (lb)'], 75 | ['3205', 'NET WEIGHT (lb)'], 76 | ['3210', 'LENGTH (in)'], 77 | ['3211', 'LENGTH (in)'], 78 | ['3212', 'LENGTH (in)'], 79 | ['3213', 'LENGTH (in)'], 80 | ['3214', 'LENGTH (in)'], 81 | ['3215', 'LENGTH (in)'], 82 | ['3220', 'LENGTH (ft)'], 83 | ['3221', 'LENGTH (ft)'], 84 | ['3222', 'LENGTH (ft)'], 85 | ['3223', 'LENGTH (ft)'], 86 | ['3224', 'LENGTH (ft)'], 87 | ['3225', 'LENGTH (ft)'], 88 | ['3230', 'LENGTH (yd)'], 89 | ['3231', 'LENGTH (yd)'], 90 | ['3232', 'LENGTH (yd)'], 91 | ['3233', 'LENGTH (yd)'], 92 | ['3234', 'LENGTH (yd)'], 93 | ['3235', 'LENGTH (yd)'], 94 | ['3240', 'WIDTH (in)'], 95 | ['3241', 'WIDTH (in)'], 96 | ['3242', 'WIDTH (in)'], 97 | ['3243', 'WIDTH (in)'], 98 | ['3244', 'WIDTH (in)'], 99 | ['3245', 'WIDTH (in)'], 100 | ['3250', 'WIDTH (ft)'], 101 | ['3251', 'WIDTH (ft)'], 102 | ['3252', 'WIDTH (ft)'], 103 | ['3253', 'WIDTH (ft)'], 104 | ['3254', 'WIDTH (ft)'], 105 | ['3255', 'WIDTH (ft)'], 106 | ['3260', 'WIDTH (yd)'], 107 | ['3261', 'WIDTH (yd)'], 108 | ['3262', 'WIDTH (yd)'], 109 | ['3263', 'WIDTH (yd)'], 110 | ['3264', 'WIDTH (yd)'], 111 | ['3265', 'WIDTH (yd)'], 112 | ['3270', 'HEIGHT (in)'], 113 | ['3271', 'HEIGHT (in)'], 114 | ['3272', 'HEIGHT (in)'], 115 | ['3273', 'HEIGHT (in)'], 116 | ['3274', 'HEIGHT (in)'], 117 | ['3275', 'HEIGHT (in)'], 118 | ['3280', 'HEIGHT (ft)'], 119 | ['3281', 'HEIGHT (ft)'], 120 | ['3282', 'HEIGHT (ft)'], 121 | ['3283', 'HEIGHT (ft)'], 122 | ['3284', 'HEIGHT (ft)'], 123 | ['3285', 'HEIGHT (ft)'], 124 | ['3290', 'HEIGHT (yd)'], 125 | ['3291', 'HEIGHT (yd)'], 126 | ['3292', 'HEIGHT (yd)'], 127 | ['3293', 'HEIGHT (yd)'], 128 | ['3294', 'HEIGHT (yd)'], 129 | ['3295', 'HEIGHT (yd)'], 130 | ['3300', 'GROSS WEIGHT (kg)'], 131 | ['3301', 'GROSS WEIGHT (kg)'], 132 | ['3302', 'GROSS WEIGHT (kg)'], 133 | ['3303', 'GROSS WEIGHT (kg)'], 134 | ['3304', 'GROSS WEIGHT (kg)'], 135 | ['3305', 'GROSS WEIGHT (kg)'], 136 | ['3310', 'LENGTH (m), log'], 137 | ['3311', 'LENGTH (m), log'], 138 | ['3312', 'LENGTH (m), log'], 139 | ['3313', 'LENGTH (m), log'], 140 | ['3314', 'LENGTH (m), log'], 141 | ['3315', 'LENGTH (m), log'], 142 | ['3320', 'WIDTH (m), log'], 143 | ['3321', 'WIDTH (m), log'], 144 | ['3322', 'WIDTH (m), log'], 145 | ['3323', 'WIDTH (m), log'], 146 | ['3324', 'WIDTH (m), log'], 147 | ['3325', 'WIDTH (m), log'], 148 | ['3330', 'HEIGHT (m), log'], 149 | ['3331', 'HEIGHT (m), log'], 150 | ['3332', 'HEIGHT (m), log'], 151 | ['3333', 'HEIGHT (m), log'], 152 | ['3334', 'HEIGHT (m), log'], 153 | ['3335', 'HEIGHT (m), log'], 154 | ['3340', 'AREA (m^2), log'], 155 | ['3341', 'AREA (m^2), log'], 156 | ['3342', 'AREA (m^2), log'], 157 | ['3343', 'AREA (m^2), log'], 158 | ['3344', 'AREA (m^2), log'], 159 | ['3345', 'AREA (m^2), log'], 160 | ['3350', 'VOLUME (l), log'], 161 | ['3351', 'VOLUME (l), log'], 162 | ['3352', 'VOLUME (l), log'], 163 | ['3353', 'VOLUME (l), log'], 164 | ['3354', 'VOLUME (l), log'], 165 | ['3355', 'VOLUME (l), log'], 166 | ['3360', 'VOLUME (m^3), log'], 167 | ['3361', 'VOLUME (m^3), log'], 168 | ['3362', 'VOLUME (m^3), log'], 169 | ['3363', 'VOLUME (m^3), log'], 170 | ['3364', 'VOLUME (m^3), log'], 171 | ['3365', 'VOLUME (m^3), log'], 172 | ['3370', 'KG PER m^2'], 173 | ['3371', 'KG PER m^2'], 174 | ['3372', 'KG PER m^2'], 175 | ['3373', 'KG PER m^2'], 176 | ['3374', 'KG PER m^2'], 177 | ['3375', 'KG PER m^2'], 178 | ['3400', 'GROSS WEIGHT (lb)'], 179 | ['3401', 'GROSS WEIGHT (lb)'], 180 | ['3402', 'GROSS WEIGHT (lb)'], 181 | ['3403', 'GROSS WEIGHT (lb)'], 182 | ['3404', 'GROSS WEIGHT (lb)'], 183 | ['3405', 'GROSS WEIGHT (lb)'], 184 | ['3410', 'LENGTH (in), log'], 185 | ['3411', 'LENGTH (in), log'], 186 | ['3412', 'LENGTH (in), log'], 187 | ['3413', 'LENGTH (in), log'], 188 | ['3414', 'LENGTH (in), log'], 189 | ['3415', 'LENGTH (in), log'], 190 | ['3420', 'LENGTH (ft), log'], 191 | ['3421', 'LENGTH (ft), log'], 192 | ['3422', 'LENGTH (ft), log'], 193 | ['3423', 'LENGTH (ft), log'], 194 | ['3424', 'LENGTH (ft), log'], 195 | ['3425', 'LENGTH (ft), log'], 196 | ['3430', 'LENGTH (yd), log'], 197 | ['3431', 'LENGTH (yd), log'], 198 | ['3432', 'LENGTH (yd), log'], 199 | ['3433', 'LENGTH (yd), log'], 200 | ['3434', 'LENGTH (yd), log'], 201 | ['3435', 'LENGTH (yd), log'], 202 | ['3440', 'WIDTH (in), log'], 203 | ['3441', 'WIDTH (in), log'], 204 | ['3442', 'WIDTH (in), log'], 205 | ['3443', 'WIDTH (in), log'], 206 | ['3444', 'WIDTH (in), log'], 207 | ['3445', 'WIDTH (in), log'], 208 | ['3450', 'WIDTH (ft), log'], 209 | ['3451', 'WIDTH (ft), log'], 210 | ['3452', 'WIDTH (ft), log'], 211 | ['3453', 'WIDTH (ft), log'], 212 | ['3454', 'WIDTH (ft), log'], 213 | ['3455', 'WIDTH (ft), log'], 214 | ['3460', 'WIDTH (yd), log'], 215 | ['3461', 'WIDTH (yd), log'], 216 | ['3462', 'WIDTH (yd), log'], 217 | ['3463', 'WIDTH (yd), log'], 218 | ['3464', 'WIDTH (yd), log'], 219 | ['3465', 'WIDTH (yd), log'], 220 | ['3470', 'HEIGHT (in), log'], 221 | ['3471', 'HEIGHT (in), log'], 222 | ['3472', 'HEIGHT (in), log'], 223 | ['3473', 'HEIGHT (in), log'], 224 | ['3474', 'HEIGHT (in), log'], 225 | ['3475', 'HEIGHT (in), log'], 226 | ['3480', 'HEIGHT (ft), log'], 227 | ['3481', 'HEIGHT (ft), log'], 228 | ['3482', 'HEIGHT (ft), log'], 229 | ['3483', 'HEIGHT (ft), log'], 230 | ['3484', 'HEIGHT (ft), log'], 231 | ['3485', 'HEIGHT (ft), log'], 232 | ['3490', 'HEIGHT (yd), log'], 233 | ['3491', 'HEIGHT (yd), log'], 234 | ['3492', 'HEIGHT (yd), log'], 235 | ['3493', 'HEIGHT (yd), log'], 236 | ['3494', 'HEIGHT (yd), log'], 237 | ['3495', 'HEIGHT (yd), log'], 238 | ['3500', 'AREA (in^2)'], 239 | ['3501', 'AREA (in^2)'], 240 | ['3502', 'AREA (in^2)'], 241 | ['3503', 'AREA (in^2)'], 242 | ['3504', 'AREA (in^2)'], 243 | ['3505', 'AREA (in^2)'], 244 | ['3510', 'AREA (ft^2)'], 245 | ['3511', 'AREA (ft^2)'], 246 | ['3512', 'AREA (ft^2)'], 247 | ['3513', 'AREA (ft^2)'], 248 | ['3514', 'AREA (ft^2)'], 249 | ['3515', 'AREA (ft^2)'], 250 | ['3520', 'AREA (yd^2)'], 251 | ['3521', 'AREA (yd^2)'], 252 | ['3522', 'AREA (yd^2)'], 253 | ['3523', 'AREA (yd^2)'], 254 | ['3524', 'AREA (yd^2)'], 255 | ['3525', 'AREA (yd^2)'], 256 | ['3530', 'AREA (in^2), log'], 257 | ['3531', 'AREA (in^2), log'], 258 | ['3532', 'AREA (in^2), log'], 259 | ['3533', 'AREA (in^2), log'], 260 | ['3534', 'AREA (in^2), log'], 261 | ['3535', 'AREA (in^2), log'], 262 | ['3540', 'AREA (ft^2), log'], 263 | ['3541', 'AREA (ft^2), log'], 264 | ['3542', 'AREA (ft^2), log'], 265 | ['3543', 'AREA (ft^2), log'], 266 | ['3544', 'AREA (ft^2), log'], 267 | ['3545', 'AREA (ft^2), log'], 268 | ['3550', 'AREA (yd^2), log'], 269 | ['3551', 'AREA (yd^2), log'], 270 | ['3552', 'AREA (yd^2), log'], 271 | ['3553', 'AREA (yd^2), log'], 272 | ['3554', 'AREA (yd^2), log'], 273 | ['3555', 'AREA (yd^2), log'], 274 | ['3560', 'NET WEIGHT (t oz)'], 275 | ['3561', 'NET WEIGHT (t oz)'], 276 | ['3562', 'NET WEIGHT (t oz)'], 277 | ['3563', 'NET WEIGHT (t oz)'], 278 | ['3564', 'NET WEIGHT (t oz)'], 279 | ['3565', 'NET WEIGHT (t oz)'], 280 | ['3570', 'NET VOLUME (oz)'], 281 | ['3571', 'NET VOLUME (oz)'], 282 | ['3572', 'NET VOLUME (oz)'], 283 | ['3573', 'NET VOLUME (oz)'], 284 | ['3574', 'NET VOLUME (oz)'], 285 | ['3575', 'NET VOLUME (oz)'], 286 | ['3600', 'NET VOLUME (qt)'], 287 | ['3601', 'NET VOLUME (qt)'], 288 | ['3602', 'NET VOLUME (qt)'], 289 | ['3603', 'NET VOLUME (qt)'], 290 | ['3604', 'NET VOLUME (qt)'], 291 | ['3605', 'NET VOLUME (qt)'], 292 | ['3610', 'NET VOLUME (gal)'], 293 | ['3611', 'NET VOLUME (gal)'], 294 | ['3612', 'NET VOLUME (gal)'], 295 | ['3613', 'NET VOLUME (gal)'], 296 | ['3614', 'NET VOLUME (gal)'], 297 | ['3615', 'NET VOLUME (gal)'], 298 | ['3620', 'VOLUME (qt), log'], 299 | ['3621', 'VOLUME (qt), log'], 300 | ['3622', 'VOLUME (qt), log'], 301 | ['3623', 'VOLUME (qt), log'], 302 | ['3624', 'VOLUME (qt), log'], 303 | ['3625', 'VOLUME (qt), log'], 304 | ['3630', 'VOLUME (gal), log'], 305 | ['3631', 'VOLUME (gal), log'], 306 | ['3632', 'VOLUME (gal), log'], 307 | ['3633', 'VOLUME (gal), log'], 308 | ['3634', 'VOLUME (gal), log'], 309 | ['3635', 'VOLUME (gal), log'], 310 | ['3640', 'VOLUME (in^3)'], 311 | ['3641', 'VOLUME (in^3)'], 312 | ['3642', 'VOLUME (in^3)'], 313 | ['3643', 'VOLUME (in^3)'], 314 | ['3644', 'VOLUME (in^3)'], 315 | ['3645', 'VOLUME (in^3)'], 316 | ['3650', 'VOLUME (ft^3)'], 317 | ['3651', 'VOLUME (ft^3)'], 318 | ['3652', 'VOLUME (ft^3)'], 319 | ['3653', 'VOLUME (ft^3)'], 320 | ['3654', 'VOLUME (ft^3)'], 321 | ['3655', 'VOLUME (ft^3)'], 322 | ['3660', 'VOLUME (yd^3)'], 323 | ['3661', 'VOLUME (yd^3)'], 324 | ['3662', 'VOLUME (yd^3)'], 325 | ['3663', 'VOLUME (yd^3)'], 326 | ['3664', 'VOLUME (yd^3)'], 327 | ['3665', 'VOLUME (yd^3)'], 328 | ['3670', 'VOLUME (in^3), log'], 329 | ['3671', 'VOLUME (in^3), log'], 330 | ['3672', 'VOLUME (in^3), log'], 331 | ['3673', 'VOLUME (in^3), log'], 332 | ['3674', 'VOLUME (in^3), log'], 333 | ['3675', 'VOLUME (in^3), log'], 334 | ['3680', 'VOLUME (ft^3), log'], 335 | ['3681', 'VOLUME (ft^3), log'], 336 | ['3682', 'VOLUME (ft^3), log'], 337 | ['3683', 'VOLUME (ft^3), log'], 338 | ['3684', 'VOLUME (ft^3), log'], 339 | ['3685', 'VOLUME (ft^3), log'], 340 | ['3690', 'VOLUME (yd^3), log'], 341 | ['3691', 'VOLUME (yd^3), log'], 342 | ['3692', 'VOLUME (yd^3), log'], 343 | ['3693', 'VOLUME (yd^3), log'], 344 | ['3694', 'VOLUME (yd^3), log'], 345 | ['3695', 'VOLUME (yd^3), log'], 346 | ['3696', 'VOLUME (yd^3), log'], 347 | ['37', 'COUNT'], 348 | ['3900', 'AMOUNT'], 349 | ['3901', 'AMOUNT'], 350 | ['3902', 'AMOUNT'], 351 | ['3903', 'AMOUNT'], 352 | ['3904', 'AMOUNT'], 353 | ['3905', 'AMOUNT'], 354 | ['3906', 'AMOUNT'], 355 | ['3907', 'AMOUNT'], 356 | ['3908', 'AMOUNT'], 357 | ['3909', 'AMOUNT'], 358 | ['3910', 'AMOUNT'], 359 | ['3911', 'AMOUNT'], 360 | ['3912', 'AMOUNT'], 361 | ['3913', 'AMOUNT'], 362 | ['3914', 'AMOUNT'], 363 | ['3915', 'AMOUNT'], 364 | ['3916', 'AMOUNT'], 365 | ['3917', 'AMOUNT'], 366 | ['3918', 'AMOUNT'], 367 | ['3919', 'AMOUNT'], 368 | ['3920', 'PRICE'], 369 | ['3921', 'PRICE'], 370 | ['3922', 'PRICE'], 371 | ['3923', 'PRICE'], 372 | ['3924', 'PRICE'], 373 | ['3925', 'PRICE'], 374 | ['3926', 'PRICE'], 375 | ['3927', 'PRICE'], 376 | ['3928', 'PRICE'], 377 | ['3929', 'PRICE'], 378 | ['3930', 'PRICE'], 379 | ['3931', 'PRICE'], 380 | ['3932', 'PRICE'], 381 | ['3933', 'PRICE'], 382 | ['3934', 'PRICE'], 383 | ['3935', 'PRICE'], 384 | ['3936', 'PRICE'], 385 | ['3937', 'PRICE'], 386 | ['3938', 'PRICE'], 387 | ['3939', 'PRICE'], 388 | ['3940', 'PRCNT OFF'], 389 | ['3941', 'PRCNT OFF'], 390 | ['3942', 'PRCNT OFF'], 391 | ['3943', 'PRCNT OFF'], 392 | ['400', 'ORDER NUMBER'], 393 | ['401', 'GINC'], 394 | ['402', 'GSIN'], 395 | ['403', 'ROUTE'], 396 | ['410', 'SHIP TO LOC'], 397 | ['411', 'BILL TO'], 398 | ['412', 'PURCHASE FROM'], 399 | ['413', 'SHIP FOR LOC'], 400 | ['414', 'LOC No'], 401 | ['415', 'PAY TO'], 402 | ['416', 'PROD/SERV LOC'], 403 | ['420', 'SHIP TO POST'], 404 | ['421', 'SHIP TO POST'], 405 | ['422', 'ORIGIN'], 406 | ['423', 'COUNTRY - INITIAL PROCESS.'], 407 | ['424', 'COUNTRY - PROCESS.'], 408 | ['425', 'COUNTRY - DISASSEMBLY'], 409 | ['426', 'COUNTRY - FULL PROCESS'], 410 | ['427', 'ORIGIN SUBDIVISION'], 411 | ['7001', 'NSN'], 412 | ['7002', 'MEAT CUT'], 413 | ['7003', 'EXPIRY TIME'], 414 | ['7004', 'ACTIVE POTENCY'], 415 | ['7005', 'CATCH AREA'], 416 | ['7006', 'FIRST FREEZE DATE'], 417 | ['7007', 'HARVEST DATE'], 418 | ['7008', 'AQUATIC SPECIES'], 419 | ['7009', 'FISHING GEAR TYPE'], 420 | ['7010', 'PROD METHOD'], 421 | ['7020', 'REFURB LOT'], 422 | ['7021', 'FUNC STAT'], 423 | ['7022', 'REV STAT'], 424 | ['7023', 'GIAI - ASSEMBLY'], 425 | ['7030', 'PROCESSOR # 0'], 426 | ['7031', 'PROCESSOR # 1'], 427 | ['7032', 'PROCESSOR # 2'], 428 | ['7033', 'PROCESSOR # 3'], 429 | ['7034', 'PROCESSOR # 4'], 430 | ['7035', 'PROCESSOR # 5'], 431 | ['7036', 'PROCESSOR # 6'], 432 | ['7037', 'PROCESSOR # 7'], 433 | ['7038', 'PROCESSOR # 8'], 434 | ['7039', 'PROCESSOR # 9'], 435 | ['7040', 'UIC+EXT'], 436 | ['710', 'NHRN PZN'], 437 | ['711', 'NHRN CIP'], 438 | ['712', 'NHRN CN'], 439 | ['713', 'NHRN DRN'], 440 | ['714', 'NHRN AIM'], 441 | ['7230', 'CERT # 0'], 442 | ['7231', 'CERT # 1'], 443 | ['7232', 'CERT # 2'], 444 | ['7233', 'CERT # 3'], 445 | ['7234', 'CERT # 4'], 446 | ['7235', 'CERT # 5'], 447 | ['7236', 'CERT # 6'], 448 | ['7237', 'CERT # 7'], 449 | ['7238', 'CERT # 8'], 450 | ['7239', 'CERT # 9'], 451 | ['7240', 'PROTOCOL'], 452 | ['8001', 'DIMENSIONS'], 453 | ['8002', 'CMT No'], 454 | ['8003', 'GRAI'], 455 | ['8004', 'GIAI'], 456 | ['8005', 'PRICE PER UNIT'], 457 | ['8006', 'ITIP'], 458 | ['8007', 'IBAN'], 459 | ['8008', 'PROD TIME'], 460 | ['8009', 'OPTSEN'], 461 | ['8010', 'CPID'], 462 | ['8011', 'CPID SERIAL'], 463 | ['8012', 'VERSION'], 464 | ['8013', 'GMN'], 465 | ['8017', 'GSRN - PROVIDER'], 466 | ['8018', 'GSRN - RECIPIENT'], 467 | ['8019', 'SRIN'], 468 | ['8020', 'REF No'], 469 | ['8026', 'ITIP CONTENT'], 470 | ['8110', ''], 471 | ['8111', 'POINTS'], 472 | ['8112', ''], 473 | ['8200', 'PRODUCT URL'], 474 | ['90', 'INTERNAL'], 475 | ['91', 'INTERNAL'], 476 | ['92', 'INTERNAL'], 477 | ['93', 'INTERNAL'], 478 | ['94', 'INTERNAL'], 479 | ['95', 'INTERNAL'], 480 | ['96', 'INTERNAL'], 481 | ['97', 'INTERNAL'], 482 | ['98', 'INTERNAL'], 483 | ['99', 'INTERNAL']])('%s is identified as %s', (expectedAi, expectedTitle) => { 484 | const { 485 | ai, 486 | title, 487 | parser, 488 | } = parseAi(expectedAi) 489 | expect(ai).toBe(expectedAi) 490 | expect(title).toBe(expectedTitle) 491 | expect(parser).toBeFunction() 492 | }) 493 | 494 | describe('custom parsers', () => { 495 | it('appends human readable values for AI 7010', () => { 496 | const { parser } = parseAi('7010') 497 | expect(parser({ barcode: '02' })).toMatchObject({ 498 | value: '02', 499 | human: 'Caught in Fresh Water', 500 | }) 501 | }) 502 | }) 503 | -------------------------------------------------------------------------------- /src/gs1/dataReaders.js: -------------------------------------------------------------------------------- 1 | 2 | const injectDecimal = require('./utils/injectDecimal') 3 | 4 | exports.fixedLength = (length) => ({ 5 | barcode, 6 | fnc = String.fromCharCode(29), 7 | }) => { 8 | const value = barcode.slice(0, length) 9 | 10 | return { 11 | value, 12 | raw: ( 13 | // Take superfluous FNC into consideration 14 | barcode.slice(value.length).startsWith(fnc) 15 | ? [value, fnc].join('') 16 | : value 17 | ), 18 | } 19 | } 20 | 21 | exports.variableLength = (maxLength) => ({ 22 | barcode, 23 | fnc = String.fromCharCode(29), 24 | }) => { 25 | const characters = [] 26 | 27 | for (const character of barcode) { 28 | if ((character === fnc || (fnc.length > 1 && character === fnc[0])) || characters.length === maxLength) break 29 | characters.push(character) 30 | } 31 | 32 | const value = characters.join('') 33 | // We may have reached end of code without finding FNC1, 34 | // but that may be okay too so lets return the whole thing as-is. 35 | return { 36 | value, 37 | raw: ( 38 | barcode.slice(value.length).startsWith(fnc) 39 | ? [value, fnc].join('') 40 | : value 41 | ), 42 | } 43 | } 44 | 45 | exports.fixedLengthDecimal = (length, decimalPositionFromEnd) => ({ 46 | barcode, 47 | fnc = String.fromCharCode(29), 48 | }) => { 49 | const { value: originalValue, raw } = exports.fixedLength(length)({ barcode, fnc }) 50 | const value = injectDecimal(originalValue, decimalPositionFromEnd) 51 | return { 52 | value, 53 | raw, 54 | } 55 | } 56 | 57 | exports.variableLengthDecimal = (maxLength, decimalPositionFromEnd) => ({ 58 | barcode, 59 | fnc = String.fromCharCode(29), 60 | }) => { 61 | const { value: originalValue, raw } = exports.variableLength(maxLength)({ barcode, fnc }) 62 | const value = injectDecimal(originalValue, decimalPositionFromEnd) 63 | 64 | return { 65 | value, 66 | raw, 67 | } 68 | } 69 | 70 | /** 71 | * Parses codes where the first three digits represent an ISO 4217 currency. 72 | */ 73 | exports.variableLengthISOCurrency = (maxLength, decimalPositionFromEnd) => ({ 74 | barcode, 75 | fnc = String.fromCharCode(29), 76 | }) => { 77 | const isoCurrencyCode = barcode.slice(0, 3) 78 | const { value: amount, raw } = exports.variableLengthDecimal(maxLength - 3, decimalPositionFromEnd)({ 79 | barcode: barcode.slice(3), 80 | fnc, 81 | }) 82 | 83 | return { 84 | value: `${isoCurrencyCode}${amount}`, 85 | isoCurrencyCode, 86 | amount, 87 | raw: `${isoCurrencyCode}${raw}`, 88 | } 89 | } 90 | 91 | /** 92 | * Parses codes where the first three digits represent an ISO 3166 country code. 93 | */ 94 | exports.variableLengthISOCountry = (maxLength) => ({ 95 | barcode, 96 | fnc = String.fromCharCode(29), 97 | }) => { 98 | const isoCountryCode = barcode.slice(0, 3) 99 | const { value, raw } = exports.variableLength(maxLength - 3)({ 100 | barcode: barcode.slice(3), 101 | fnc, 102 | }) 103 | 104 | return { 105 | value, 106 | isoCountryCode, 107 | raw: `${isoCountryCode}${raw}`, 108 | } 109 | } 110 | 111 | /** 112 | * Parses YYMMDD 113 | */ 114 | exports.date = () => ({ 115 | barcode, 116 | fnc = String.fromCharCode(29), 117 | }) => { 118 | const { value: yymmdd, raw } = exports.fixedLength(6)({ barcode, fnc }) 119 | 120 | const year = parseInt(yymmdd.slice(0, 2), 10) 121 | const month = yymmdd.slice(2, 4) 122 | const day = yymmdd.slice(4, 6) 123 | 124 | // section 7.12 of the specification states that 125 | // years 51-99 should be considered to belong to the 1900s 126 | const century = (year > 50 ? 1900 : 2000) 127 | 128 | const value = `${year + century}-${month}-${day}` 129 | return { 130 | value, 131 | raw, 132 | } 133 | } 134 | 135 | /** 136 | * Parses YYMMDDHH(mmss) 137 | * 138 | * @param optionalMinutesAndSeconds By default, YYMMDDHHmm is expected. This flag enables YYMMDDHH[mmss] 139 | * @returns "YYYY-MM-DD HH:mm:ss" 140 | */ 141 | exports.dateTime = ({ optionalMinutesAndSeconds = false } = {}) => ({ 142 | barcode, 143 | fnc = String.fromCharCode(29), 144 | }) => { 145 | const { value: yymmddhhmm, raw } = exports.fixedLength(optionalMinutesAndSeconds ? 12 : 10)({ barcode, fnc }) 146 | 147 | const { value: yymmdd } = exports.date()({ 148 | barcode: yymmddhhmm.slice(0, 6), 149 | fnc, 150 | }) 151 | 152 | const hh = yymmddhhmm.slice(6, 8) 153 | const mm = yymmddhhmm.slice(8, 10) || (optionalMinutesAndSeconds && '00') 154 | const ss = yymmddhhmm.slice(10) || '00' 155 | 156 | return { 157 | value: `${yymmdd} ${hh}:${mm}:${ss}`, 158 | raw, 159 | } 160 | } 161 | 162 | /** 163 | * Parses YYMMDDYYMMDD (Start-End) and YYMMDD (Start and End on same date) 164 | */ 165 | exports.dateRange = () => ({ 166 | barcode, 167 | fnc = String.fromCharCode(29), 168 | }) => { 169 | const varLength = exports.variableLength(12)({ barcode, fnc }) 170 | const hasTwoDates = varLength.value.length > 6 171 | const firstDate = exports.date()({ barcode: barcode.slice(0, 6), fnc }) 172 | const secondDate = exports.date()({ barcode: barcode.slice(6), fnc }) 173 | 174 | // "In case the period spans one calendar day, the end date SHALL NOT be specified." - section 3.8.8 175 | const value = `${firstDate.value}${hasTwoDates ? ' - ' + secondDate.value : ''}` 176 | return { 177 | value, 178 | raw: varLength.raw, 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/gs1/dataReaders.spec.js: -------------------------------------------------------------------------------- 1 | 2 | const { 3 | fixedLength, 4 | variableLength, 5 | variableLengthDecimal, 6 | fixedLengthDecimal, 7 | variableLengthISOCurrency, 8 | variableLengthISOCountry, 9 | date, 10 | dateTime, 11 | dateRange, 12 | } = require('./dataReaders') 13 | 14 | const expectedReaders = [ 15 | ['fixedLength', fixedLength], 16 | ['variableLength', variableLength], 17 | ['fixedLengthDecimal', fixedLengthDecimal], 18 | ['variableLengthDecimal', variableLengthDecimal], 19 | ['variableLengthISOCurrency', variableLengthISOCurrency], 20 | ['variableLengthISOCountry', variableLengthISOCountry], 21 | ['date', date], 22 | ['dateTime', dateTime], 23 | ['dateRange', dateRange], 24 | ] 25 | 26 | it('has tests for all exported parsers', () => { 27 | // A gentle reminder to whomever. 28 | expect(Object.keys(require('./dataReaders'))).toEqual(expectedReaders.map(([readerName]) => readerName)) 29 | }) 30 | 31 | describe.each(expectedReaders)('%s()', (readerName, readerFn) => { 32 | it('is a curry-function', () => { 33 | expect(readerFn).toBeInstanceOf(Function) 34 | expect(readerFn()).toBeInstanceOf(Function) 35 | }) 36 | }) 37 | 38 | describe('fixedLength()', () => { 39 | it('throws when barcode is omitted', () => { 40 | expect(() => { 41 | fixedLength(1337)() 42 | }).toThrow() 43 | }) 44 | it('returns object with expected props', () => { 45 | const parser = fixedLength(5) 46 | const returnedObj = parser({ barcode: '1234567890' }) 47 | expect(returnedObj).toContainKeys([ 48 | 'value', 49 | 'raw', 50 | ]) 51 | }) 52 | it('reads a fixed length of the barcode', () => { 53 | const parser = fixedLength(5) 54 | const { value } = parser({ barcode: '1234567890' }) 55 | expect(value).toBe('12345') 56 | }) 57 | it('accepts an additional FNC beyond the length', () => { 58 | const FNC = String.fromCharCode(29) 59 | const parser = fixedLength(5) 60 | const { value, raw } = parser({ barcode: `12345${FNC}67890` }) 61 | expect(value).toBe('12345') 62 | expect(raw).toBe(`12345${FNC}`) 63 | }) 64 | }) 65 | 66 | describe('variableLength()', () => { 67 | it('throws when barcode is omitted', () => { 68 | expect(() => { 69 | variableLength()() 70 | }).toThrow() 71 | }) 72 | it('returns object with expected props', () => { 73 | const parser = variableLength() 74 | const returnedObj = parser({ barcode: `1234567890` }) 75 | expect(returnedObj).toContainKeys([ 76 | 'value', 77 | 'raw', 78 | ]) 79 | }) 80 | it('reads barcode until it encounters an FNC', () => { 81 | const FNC = String.fromCharCode(29) 82 | const parser = variableLength() 83 | const { value, raw } = parser({ barcode: `12345${FNC}67890` }) 84 | expect(value).toBe('12345') 85 | expect(raw).toBe(`12345${FNC}`) 86 | }) 87 | it('if no FNC is found, until the limit has been reached', () => { 88 | const limit = 6 89 | const parser = variableLength(limit) 90 | const { value, raw } = parser({ barcode: `1234567890` }) 91 | expect(value).toBe('123456') 92 | expect(raw).toBe('123456') 93 | }) 94 | it('if no FNC is found and barcode ends, what has been read is considered enough', () => { 95 | const limit = 30 96 | const parser = variableLength(limit) 97 | const { value, raw } = parser({ barcode: `1234567890` }) 98 | expect(value).toBe('1234567890') 99 | expect(raw).toBe('1234567890') 100 | }) 101 | }) 102 | 103 | describe('fixedLengthDecimal()', () => { 104 | it('respects the length', () => { 105 | expect(fixedLengthDecimal(2)({ barcode: '1234567890' }).value).toBe('12') 106 | }) 107 | it('respects the decimal point', () => { 108 | expect(fixedLengthDecimal(3, 1)({ barcode: '1234567890' }).value).toBe('12.3') 109 | }) 110 | }) 111 | 112 | describe('variableLengthDecimal()', () => { 113 | it('respects the maxLength', () => { 114 | expect(variableLengthDecimal(2)({ barcode: '1234567890' }).value).toBe('12') 115 | }) 116 | it('respects the decimal point', () => { 117 | expect(variableLengthDecimal(4, 1)({ barcode: '1234567890' }).value).toBe('123.4') 118 | }) 119 | it('prioritizes FNC over maxLength', () => { 120 | const FNC = String.fromCharCode(29) 121 | expect(variableLengthDecimal(4, 1)({ barcode: `123${FNC}4567890` }).value).toBe('12.3') 122 | }) 123 | }) 124 | 125 | describe('variableLengthISOCurrency()', () => { 126 | it('throws when barcode is omitted', () => { 127 | expect(() => { 128 | variableLengthISOCurrency()() 129 | }).toThrow() 130 | }) 131 | it('returns object with expected props', () => { 132 | const parser = variableLengthISOCurrency(1) 133 | const returnedObj = parser({ barcode: `1234567890` }) 134 | expect(returnedObj).toContainKeys([ 135 | 'value', 136 | 'amount', 137 | 'isoCurrencyCode', 138 | 'raw', 139 | ]) 140 | }) 141 | it('uses the first 3 characters as ISO-4217 code', () => { 142 | const parser = variableLengthISOCurrency() 143 | const { isoCurrencyCode } = parser({ barcode: `978111111111` }) 144 | expect(isoCurrencyCode).toBe('978') // EUR! :D 145 | }) 146 | it('includes additional prop for amount', () => { 147 | const parser = variableLengthISOCurrency() 148 | const { amount } = parser({ barcode: `9781337` }) 149 | expect(amount).toBe('1337') 150 | }) 151 | it('respects decimals', () => { 152 | const parser = variableLengthISOCurrency(7, 2) 153 | const { amount } = parser({ barcode: `9781337` }) 154 | expect(amount).toBe('13.37') 155 | }) 156 | }) 157 | 158 | describe('variableLengthISOCountry()', () => { 159 | it('throws when barcode is omitted', () => { 160 | expect(() => { 161 | variableLengthISOCountry()() 162 | }).toThrow() 163 | }) 164 | it('returns object with expected props', () => { 165 | const parser = variableLengthISOCountry(1) 166 | const returnedObj = parser({ barcode: `1234567890` }) 167 | expect(returnedObj).toContainKeys([ 168 | 'value', 169 | 'isoCountryCode', 170 | 'raw', 171 | ]) 172 | }) 173 | it('uses the first 3 characters as ISO-3166 code', () => { 174 | const parser = variableLengthISOCountry() 175 | const { isoCountryCode } = parser({ barcode: `752` }) 176 | expect(isoCountryCode).toBe('752') // Sweden! :D 177 | }) 178 | it('respects maxlength (including ISO code)', () => { 179 | const parser = variableLengthISOCountry(5) 180 | const { value } = parser({ barcode: `9781337` }) 181 | expect(value).toBe('13') 182 | }) 183 | }) 184 | 185 | describe('date()', () => { 186 | it('uses a fixed length of 6', () => { 187 | expect(date()({ barcode: '0102030405' }).raw).toBe('010203') 188 | }) 189 | it('returns values in YYYY-MM-DD format', () => { 190 | expect(date()({ barcode: '010203' }).value).toBe('2001-02-03') 191 | }) 192 | it('parses as YYMMDD with century-correction', () => { 193 | expect(date()({ barcode: '990203' }).value).toMatch(/^1999/) 194 | expect(date()({ barcode: '010203' }).value).toMatch(/^2001/) 195 | }) 196 | }) 197 | 198 | describe('dateTime()', () => { 199 | it('uses a fixed length of 10 by default', () => { 200 | expect(dateTime()({ barcode: '01020304050607' }).raw).toBe('0102030405') 201 | }) 202 | it('returns values in YYYY-MM-DD HH:mm:ss format', () => { 203 | expect(dateTime()({ barcode: '9001071337' }).value).toBe('1990-01-07 13:37:00') 204 | }) 205 | it('parses as YYMMDD with century-correction', () => { 206 | expect(dateTime()({ barcode: '990203' }).value).toMatch(/^1999/) 207 | expect(dateTime()({ barcode: '010203' }).value).toMatch(/^2001/) 208 | }) 209 | describe('dateTime({ optionalMinutesAndSeconds: true })', () => { 210 | it('uses a fixed length of 12', () => { 211 | expect(dateTime({ optionalMinutesAndSeconds: true })({ barcode: '01020304050607' }).raw).toBe('010203040506') 212 | }) 213 | it('allows YYMMDDHHmmss', () => { 214 | expect(dateTime({ optionalMinutesAndSeconds: true })({ barcode: '900107133755' }).value).toBe('1990-01-07 13:37:55') 215 | }) 216 | it('allows YYMMDDHHmm', () => { 217 | // (defaults seconds to 00) 218 | expect(dateTime({ optionalMinutesAndSeconds: true })({ barcode: '0011222345' }).value).toBe('2000-11-22 23:45:00') 219 | }) 220 | it('allows YYMMDDHH', () => { 221 | // (defaults minutes and seconds to 00:00) 222 | expect(dateTime({ optionalMinutesAndSeconds: true })({ barcode: '00112223' }).value).toBe('2000-11-22 23:00:00') 223 | }) 224 | }) 225 | }) 226 | 227 | describe('dateRange()', () => { 228 | it('uses a max length of 12', () => { 229 | expect(dateRange()({ barcode: '010203040506070809' }).raw).toBe('010203040506') 230 | }) 231 | it('parses as YYMMDDYYMMDD with century-correction', () => { 232 | expect(dateRange()({ barcode: '990203010405' }).value).toBe('1999-02-03 - 2001-04-05') 233 | expect(dateRange()({ barcode: '010203010204' }).value).toBe('2001-02-03 - 2001-02-04') 234 | }) 235 | it('allows YYMMDD input with FNC to denote a single date', () => { 236 | const FNC = String.fromCharCode(29) 237 | expect(dateRange()({ barcode: `010405${FNC}` })).toMatchObject({ 238 | value: '2001-04-05', 239 | raw: `010405${FNC}`, 240 | }) 241 | }) 242 | }) 243 | -------------------------------------------------------------------------------- /src/gs1/parser.js: -------------------------------------------------------------------------------- 1 | 2 | const findSymbology = require('./symbologies') 3 | const { parseAi } = require('./applicationIdentifiers') 4 | 5 | /** 6 | * @param {object} query 7 | * @param {string} query.barcode The barcode to parse. 8 | * @param {string} query.fnc The character representing FNC1. Defaults to ASCII 29 (Group Separator) 9 | */ 10 | const gs1Parser = ({ 11 | barcode: originalBarcode, 12 | fnc = String.fromCharCode(29), 13 | }) => { 14 | // First, try to derive which symbology is being used. 15 | // This is not important in itself since GS1 encodes data the 16 | // same way across all(?) symbologies, but the symbology prefix 17 | // length decides where the actual data in the barcode starts. 18 | const { 19 | symbology, 20 | remainingBarcode: barcode, 21 | } = findSymbology(originalBarcode) 22 | 23 | // Somewhere to store our scientific discoveries 24 | const elements = [] 25 | 26 | // Let's iterate the code, one AI at a time. 27 | let currPos = 0 28 | while (currPos < barcode.length) { 29 | const { ai, title, parser } = parseAi(barcode.slice(currPos)) 30 | currPos += ai.length 31 | 32 | const element = parser({ barcode: barcode.slice(currPos), fnc }) 33 | currPos += element.raw.length 34 | 35 | const currentElement = { 36 | ai, 37 | title, 38 | ...element, 39 | } 40 | elements.push(currentElement) 41 | } 42 | return { 43 | symbology, 44 | elements, 45 | originalBarcode, 46 | } 47 | } 48 | 49 | module.exports = exports = gs1Parser 50 | -------------------------------------------------------------------------------- /src/gs1/parser.spec.js: -------------------------------------------------------------------------------- 1 | 2 | const parser = require('./parser') 3 | 4 | describe('unit', () => { 5 | it('figures out GTIN', () => { 6 | expect(parser({ barcode: '0112345678901234' })).toMatchObject({ 7 | symbology: 'unknown', 8 | elements: [ 9 | { 10 | ai: '01', 11 | raw: '12345678901234', 12 | title: 'GTIN', 13 | value: '12345678901234', 14 | }, 15 | ], 16 | originalBarcode: '0112345678901234', 17 | }) 18 | }) 19 | 20 | it('figures out GTIN with a QR symbology prefix', () => { 21 | expect(parser({ barcode: ']Q30112345678901234' })).toMatchObject({ 22 | symbology: 'GS1 QR Code', 23 | elements: [ 24 | { 25 | ai: '01', 26 | raw: '12345678901234', 27 | title: 'GTIN', 28 | value: '12345678901234', 29 | }, 30 | ], 31 | originalBarcode: ']Q30112345678901234', 32 | }) 33 | }) 34 | 35 | it('figures out GTIN and BATCH/LOT in the same code', () => { 36 | expect(parser({ barcode: '0112345678901234101337' })).toMatchObject({ 37 | symbology: 'unknown', 38 | elements: [ 39 | { 40 | ai: '01', 41 | raw: '12345678901234', 42 | title: 'GTIN', 43 | value: '12345678901234', 44 | }, 45 | { 46 | ai: '10', 47 | raw: '1337', 48 | title: 'BATCH/LOT', 49 | value: '1337', 50 | }, 51 | ], 52 | originalBarcode: '0112345678901234101337', 53 | }) 54 | }) 55 | 56 | it('figures out GTIN and BATCH/LOT in the same code', () => { 57 | expect(parser({ barcode: '0112345678901234101337' })).toMatchObject({ 58 | symbology: 'unknown', 59 | elements: [ 60 | { 61 | ai: '01', 62 | raw: '12345678901234', 63 | title: 'GTIN', 64 | value: '12345678901234', 65 | }, 66 | { 67 | ai: '10', 68 | raw: '1337', 69 | title: 'BATCH/LOT', 70 | value: '1337', 71 | }, 72 | ], 73 | originalBarcode: '0112345678901234101337', 74 | }) 75 | }) 76 | 77 | it('handles cases where variablelength occurs first', () => { 78 | const FNC = String.fromCharCode(29) 79 | expect(parser({ barcode: `101337${FNC}0112345678901234` })).toMatchObject({ 80 | symbology: 'unknown', 81 | elements: [ 82 | { 83 | ai: '10', 84 | raw: `1337${FNC}`, 85 | title: 'BATCH/LOT', 86 | value: '1337', 87 | }, 88 | { 89 | ai: '01', 90 | raw: '12345678901234', 91 | title: 'GTIN', 92 | value: '12345678901234', 93 | }, 94 | ], 95 | originalBarcode: `101337${FNC}0112345678901234`, 96 | }) 97 | }) 98 | 99 | it('allows multi-character FNC', () => { 100 | const fnc = '{GS}' 101 | const barcode = `107473020${fnc}217473020-000` 102 | 103 | expect(parser({ barcode, fnc })).toMatchObject({ 104 | elements: [ 105 | { 106 | ai: '10', 107 | raw: `7473020${fnc}`, 108 | title: 'BATCH/LOT', 109 | value: '7473020', 110 | }, 111 | { 112 | ai: '21', 113 | raw: '7473020-000', 114 | title: 'SERIAL', 115 | value: '7473020-000', 116 | }, 117 | ], 118 | }) 119 | }) 120 | 121 | it('allows multibyte (emoji) FNC', () => { 122 | const fnc = '🥉' 123 | const barcode = `107473020${fnc}217473020-000` 124 | 125 | expect(parser({ barcode, fnc })).toMatchObject({ 126 | elements: [ 127 | { 128 | ai: '10', 129 | raw: `7473020${fnc}`, 130 | title: 'BATCH/LOT', 131 | value: '7473020', 132 | }, 133 | { 134 | ai: '21', 135 | raw: '7473020-000', 136 | title: 'SERIAL', 137 | value: '7473020-000', 138 | }, 139 | ], 140 | }) 141 | }) 142 | }) 143 | 144 | describe('live examples parse as expected', () => { 145 | const FNC = String.fromCharCode(29) 146 | 147 | // https://www.packagingstrategies.com/ext/resources/ISSUES/2017/10-October/MPC-C-2DBarcode.jpg 148 | it('(21)123456789012{FNC}(17)131000(10)A1234567', () => { 149 | const parsed = parser({ barcode: `]d221123456789012${FNC}1713100010A1234567` }) 150 | expect(parsed).toMatchObject({ 151 | symbology: 'GS1 DataMatrix', 152 | elements: [ 153 | { 154 | ai: '21', 155 | raw: `123456789012${FNC}`, 156 | title: 'SERIAL', 157 | value: '123456789012', 158 | }, 159 | { 160 | ai: '17', 161 | title: 'USE BY OR EXPIRY', 162 | value: '2013-10-00', 163 | }, 164 | { 165 | ai: '10', 166 | raw: 'A1234567', 167 | title: 'BATCH/LOT', 168 | value: 'A1234567', 169 | }, 170 | ], 171 | }) 172 | }) 173 | 174 | // http://support.efficientbi.com/wp-content/uploads/2D-GS1-Code-Sample.png 175 | it('(01)50311704620018(21)123456789012{FNC}(17)180531(10)S12345', () => { 176 | const parsed = parser({ barcode: `015031170462001821123456789012${FNC}1718053110S12345` }) 177 | expect(parsed).toMatchObject({ 178 | symbology: 'unknown', 179 | elements: [ 180 | { 181 | ai: '01', 182 | title: 'GTIN', 183 | value: '50311704620018', 184 | raw: `50311704620018`, 185 | }, 186 | { 187 | ai: '21', 188 | title: 'SERIAL', 189 | value: '123456789012', 190 | raw: `123456789012${FNC}`, 191 | }, 192 | { 193 | ai: '17', 194 | title: 'USE BY OR EXPIRY', 195 | value: '2018-05-31', 196 | raw: '180531', 197 | }, 198 | { 199 | ai: '10', 200 | title: 'BATCH/LOT', 201 | value: 'S12345', 202 | raw: 'S12345', 203 | }, 204 | ], 205 | }) 206 | }) 207 | 208 | // https://shop.wanderlust-webdesign.com/wp-content/uploads/2014/06/ff1hsg.png 209 | it('(420)95747(94)77707123456123456781', () => { 210 | const parsed = parser({ barcode: `42095747${FNC}9477707123456123456781` }) 211 | expect(parsed).toMatchObject({ 212 | elements: [ 213 | { 214 | ai: '420', 215 | title: 'SHIP TO POST', 216 | value: '95747', 217 | raw: `95747${FNC}`, 218 | }, 219 | { 220 | ai: '94', 221 | title: 'INTERNAL', 222 | value: '77707123456123456781', 223 | raw: `77707123456123456781`, 224 | }, 225 | ], 226 | }) 227 | }) 228 | 229 | // https://www.palletlabel.com/wp-content/uploads/2018/10/Palletlabel-SSCC-label-EDI-klein.png 230 | it('(02)05060478880004(37)66(10)123abc(00)990000100000001862', () => { 231 | const parsed = parser({ barcode: `02050604788800043766${FNC}10123abc${FNC}00990000100000001862` }) 232 | expect(parsed).toMatchObject({ 233 | symbology: 'unknown', 234 | elements: [ 235 | { 236 | ai: '02', 237 | title: 'CONTENT', 238 | value: '05060478880004', 239 | raw: `05060478880004`, 240 | }, 241 | { 242 | ai: '37', 243 | title: 'COUNT', 244 | value: '66', 245 | raw: `66${FNC}`, 246 | }, 247 | { 248 | ai: '10', 249 | title: 'BATCH/LOT', 250 | value: '123abc', 251 | raw: `123abc${FNC}`, 252 | }, 253 | { 254 | ai: '00', 255 | raw: '990000100000001862', 256 | title: 'SSCC', 257 | value: '990000100000001862', 258 | }, 259 | ], 260 | }) 261 | }) 262 | }) 263 | -------------------------------------------------------------------------------- /src/gs1/symbologies.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Looks at the start of a barcode to identify 4 | * the type of GS1 symbology is being read. 5 | * 6 | * @param {string} originalBarcode 7 | * @return {obj} The symbology name and part of barcode that remains to be parsed. 8 | */ 9 | const findSymbology = (originalBarcode) => { 10 | const symbologies = [ 11 | { prefix: ']C1', name: 'GS1-128' }, 12 | { prefix: ']e0', name: 'GS1 DataBar' }, 13 | { prefix: ']e1', name: 'GS1 Composite' }, 14 | { prefix: ']e2', name: 'GS1 Composite' }, 15 | { prefix: ']d2', name: 'GS1 DataMatrix' }, 16 | { prefix: ']Q3', name: 'GS1 QR Code' }, 17 | ] 18 | 19 | const symbologyUsed = symbologies.find(({ prefix }) => originalBarcode.startsWith(prefix)) 20 | if (symbologyUsed) { 21 | return { 22 | symbology: symbologyUsed.name, 23 | remainingBarcode: originalBarcode.slice(symbologyUsed.prefix.length), 24 | } 25 | } 26 | return { 27 | symbology: 'unknown', 28 | remainingBarcode: originalBarcode, 29 | } 30 | } 31 | 32 | module.exports = exports = findSymbology 33 | -------------------------------------------------------------------------------- /src/gs1/symbologies.spec.js: -------------------------------------------------------------------------------- 1 | 2 | const findSymbology = require('./symbologies') 3 | 4 | it('is a method', () => { 5 | expect(findSymbology).toBeFunction() 6 | }) 7 | 8 | it('returns symbology and remainder of code', () => { 9 | const result = findSymbology(`Yolo swag!`) 10 | expect(result).toContainKeys([ 11 | 'symbology', 12 | 'remainingBarcode', 13 | ]) 14 | }) 15 | 16 | it('defaults to unknown symbology', () => { 17 | const result = findSymbology(`Yolo swag!`) 18 | expect(result.symbology).toBe('unknown') 19 | expect(result.remainingBarcode).toBe('Yolo swag!') 20 | }) 21 | 22 | it.each([ 23 | [']C1', 'GS1-128'], 24 | [']e0', 'GS1 DataBar'], 25 | [']e1', 'GS1 Composite'], 26 | [']e2', 'GS1 Composite'], 27 | [']d2', 'GS1 DataMatrix'], 28 | [']Q3', 'GS1 QR Code'], 29 | ])('identifies %s as %s and returns remainder', (prefix, symbology) => { 30 | const code = '01234567890' 31 | const result = findSymbology(`${prefix}${code}`) 32 | expect(result.symbology).toBe(symbology) 33 | expect(result.remainingBarcode).toBe(code) 34 | }) 35 | -------------------------------------------------------------------------------- /src/gs1/utils/injectDecimal.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = exports = (input, decimalPositionFromEnd) => { 3 | const str = `${input}` 4 | const decPos = parseInt(decimalPositionFromEnd, 10) 5 | const injectedDecimal = ( 6 | decPos 7 | ? `${str.slice(0, -decPos)}.${str.slice(-decPos)}` 8 | : `${str}` 9 | ) 10 | return ( 11 | injectedDecimal.startsWith('.') 12 | ? `0${injectedDecimal}` 13 | : injectedDecimal 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/gs1/utils/injectDecimal.spec.js: -------------------------------------------------------------------------------- 1 | 2 | const injectDecimal = require('./injectDecimal') 3 | 4 | it('exports a function', () => { 5 | expect(injectDecimal).toBeInstanceOf(Function) 6 | }) 7 | it('returns original string when decimal point is missing', () => { 8 | expect(injectDecimal('100')).toBe('100') 9 | }) 10 | it('returns original string when decimal point is 0', () => { 11 | expect(injectDecimal('100', '0')).toBe('100') 12 | }) 13 | it('injects decimal point X characters from the end', () => { 14 | expect(injectDecimal('100', '1')).toBe('10.0') 15 | expect(injectDecimal('1337', '2')).toBe('13.37') 16 | }) 17 | it('accepts decimals at the starting position', () => { 18 | expect(injectDecimal('100', '3')).toBe('0.100') 19 | // Heck, you can even use a number larger than the string length. 20 | expect(injectDecimal('100', '4000')).toBe('0.100') 21 | }) 22 | it('accepts numerical arguments, too', () => { 23 | expect(injectDecimal(100)).toBe('100') 24 | expect(injectDecimal(103, 1)).toBe('10.3') 25 | }) 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | const gs1Parser = require('./gs1/parser') 3 | 4 | const bark = (barcode, settings = {}) => { 5 | let input = barcode 6 | 7 | const defaults = { 8 | assumeGtin: false, 9 | fnc: String.fromCharCode(29), 10 | } 11 | const options = { 12 | ...defaults, 13 | ...settings, 14 | } 15 | 16 | // Do we even want to try to infere a GTIN? 17 | if (options.assumeGtin) { 18 | const digitsOnly = /^\d+$/.test(input) 19 | if (digitsOnly) { 20 | if (input.length >= 11 && input.length <= 14) { 21 | // We'll assume its one of UPC-A, EAN-13, ITF-14 22 | input = `01${input.padStart(14, '0')}` 23 | } 24 | } 25 | } 26 | 27 | return gs1Parser({ barcode: input, fnc: options.fnc }) 28 | } 29 | 30 | module.exports = exports = bark 31 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Use `npm test` to run tests using mocha. 3 | or `./node_modules/.bin/mocha --reporter spec` 4 | */ 5 | 6 | const src = require('path').resolve(process.cwd(), 'src') 7 | const bark = require(`${src}/index.js`) 8 | 9 | describe('Bark', () => { 10 | it('should load as a module', () => { 11 | expect(bark).toBeDefined() 12 | }) 13 | 14 | describe('assumeGtin', () => { 15 | it('should parse an EAN-13', () => { 16 | const parsed = bark('3281014704901', { assumeGtin: true }) 17 | expect(parsed).toMatchObject({ 18 | elements: expect.arrayContaining([ 19 | expect.objectContaining({ title: 'GTIN', value: '03281014704901' }), 20 | ]), 21 | }) 22 | }) 23 | 24 | it('should parse an ITF14', () => { 25 | const parsed = bark('17312133015982', { assumeGtin: true }) 26 | expect(parsed).toMatchObject({ 27 | elements: expect.arrayContaining([ 28 | expect.objectContaining({ title: 'GTIN', value: '17312133015982' }), 29 | ]), 30 | }) 31 | }) 32 | 33 | it('should parse the GTIN from a GS1-128 code', () => { 34 | const parsed = bark('015730033004934115160817', { assumeGtin: true }) 35 | expect(parsed).toMatchObject({ 36 | elements: expect.arrayContaining([ 37 | expect.objectContaining({ title: 'GTIN', value: '57300330049341' }), 38 | ]), 39 | }) 40 | }) 41 | }) 42 | 43 | it('should parse the BEST BEFORE', () => { 44 | const parsed = bark('015730033004934115160817') 45 | expect(parsed).toMatchObject({ 46 | elements: expect.arrayContaining([ 47 | expect.objectContaining({ title: 'BEST BEFORE or BEST BY', value: '2016-08-17' }), 48 | ]), 49 | }) 50 | }) 51 | 52 | it('should parse the NET WEIGHT with 3 decimal points', () => { 53 | const parsed = bark('01573003300493413103160817') 54 | expect(parsed).toMatchObject({ 55 | elements: expect.arrayContaining([ 56 | expect.objectContaining({ title: 'NET WEIGHT (kg)', value: '160.817' }), 57 | ]), 58 | }) 59 | }) 60 | }) 61 | --------------------------------------------------------------------------------