@jrrembert/luhnjs 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -4
- package/dist/src/luhn.d.ts +1 -2
- package/dist/src/luhn.js +38 -30
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -5,14 +5,83 @@
|
|
|
5
5
|
|
|
6
6
|
A TypeScript implementation of the Luhn algorithm for generating and validating checksums.
|
|
7
7
|
|
|
8
|
+
Published as [`@jrrembert/luhnjs`](https://www.npmjs.com/package/@jrrembert/luhnjs) on npm.
|
|
9
|
+
|
|
10
|
+
## Getting Started
|
|
11
|
+
|
|
12
|
+
### Prerequisites
|
|
13
|
+
|
|
14
|
+
Install [Node.js](https://nodejs.org/) (>=20.x) and [Yarn](https://yarnpkg.com/).
|
|
15
|
+
|
|
16
|
+
### Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# npm
|
|
20
|
+
$ npm install @jrrembert/luhnjs
|
|
21
|
+
|
|
22
|
+
# yarn
|
|
23
|
+
$ yarn add @jrrembert/luhnjs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Usage
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import {
|
|
30
|
+
generate,
|
|
31
|
+
validate,
|
|
32
|
+
random,
|
|
33
|
+
generateModN,
|
|
34
|
+
validateModN,
|
|
35
|
+
checksumModN,
|
|
36
|
+
} from '@jrrembert/luhnjs';
|
|
37
|
+
|
|
38
|
+
// Generate a checksum
|
|
39
|
+
generate('7992739871'); // '79927398713'
|
|
40
|
+
generate('7992739871', { checkSumOnly: true }); // '3'
|
|
41
|
+
|
|
42
|
+
// Validate a checksum
|
|
43
|
+
validate('79927398713'); // true
|
|
44
|
+
|
|
45
|
+
// Generate a random number with valid checksum
|
|
46
|
+
random('16'); // e.g. '4539148803436467'
|
|
47
|
+
|
|
48
|
+
// Mod-N variants (base 2–36, supports alphanumeric)
|
|
49
|
+
generateModN('1', 16); // '1E'
|
|
50
|
+
validateModN('1E', 16); // true
|
|
51
|
+
checksumModN('12345', 10); // 5
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Install dependencies
|
|
58
|
+
$ yarn
|
|
59
|
+
|
|
60
|
+
# Run tests
|
|
61
|
+
$ yarn test
|
|
62
|
+
|
|
63
|
+
# Lint
|
|
64
|
+
$ yarn lint
|
|
65
|
+
|
|
66
|
+
# Build
|
|
67
|
+
$ yarn build
|
|
68
|
+
```
|
|
69
|
+
|
|
8
70
|
## Documentation
|
|
9
71
|
|
|
10
|
-
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
72
|
+
<!-- TODO: Add link to API reference when published -->
|
|
73
|
+
- [Specification](docs/SPEC.md) - API specification
|
|
74
|
+
- [Release Process](docs/RELEASE.md) - Automated releases via semantic-release
|
|
75
|
+
- [CI/CD](docs/CI.md) - Workflows and troubleshooting
|
|
76
|
+
- [Contributing](CONTRIBUTING.md) - How to contribute
|
|
77
|
+
- [Security](SECURITY.md) - Reporting vulnerabilities
|
|
78
|
+
|
|
79
|
+
## Contact
|
|
80
|
+
|
|
81
|
+
Email: [J. Ryan Rembert](mailto:j.ryan.rembert@gmail.com)
|
|
13
82
|
|
|
14
83
|
## License
|
|
15
84
|
|
|
16
85
|
[MIT](LICENSE)
|
|
17
86
|
|
|
18
|
-
Copyright © 2022-2026 J. Ryan Rembert
|
|
87
|
+
Copyright © 2022-2026 J. Ryan Rembert
|
package/dist/src/luhn.d.ts
CHANGED
package/dist/src/luhn.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.generate = generate;
|
|
4
|
+
exports.validate = validate;
|
|
5
|
+
exports.random = random;
|
|
6
|
+
exports.generateModN = generateModN;
|
|
7
|
+
exports.validateModN = validateModN;
|
|
8
|
+
exports.checksumModN = checksumModN;
|
|
4
9
|
const CODE_POINTS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
5
|
-
class GenerateOptions {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.checkSumOnly = false;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
10
|
/**
|
|
11
11
|
* Validates that input is a non-empty string that can be converted to a number
|
|
12
12
|
*
|
|
@@ -33,6 +33,30 @@ function handleErrors(value) {
|
|
|
33
33
|
throw new Error('string must be convertible to a number');
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Validates that input is a non-empty string with valid characters for the given modulus
|
|
38
|
+
*
|
|
39
|
+
* @param value - String to validate
|
|
40
|
+
* @param n - Modulus (determines valid character set)
|
|
41
|
+
* @throws {Error} If value is not a string, is empty, contains spaces, or has invalid characters
|
|
42
|
+
*/
|
|
43
|
+
function handleModNErrors(value, n) {
|
|
44
|
+
if (typeof value !== 'string') {
|
|
45
|
+
throw new Error(`value must be a string - received ${value}`);
|
|
46
|
+
}
|
|
47
|
+
if (!value.length) {
|
|
48
|
+
throw new Error('string cannot be empty');
|
|
49
|
+
}
|
|
50
|
+
if (value.includes(' ')) {
|
|
51
|
+
throw new Error('string cannot contain spaces');
|
|
52
|
+
}
|
|
53
|
+
const validChars = CODE_POINTS.slice(0, n);
|
|
54
|
+
for (const char of value) {
|
|
55
|
+
if (!validChars.includes(char.toUpperCase())) {
|
|
56
|
+
throw new Error(`invalid character: <${char}>`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
36
60
|
/**
|
|
37
61
|
* Generates a Luhn algorithm checksum digit for a string of numbers
|
|
38
62
|
*
|
|
@@ -74,7 +98,6 @@ function generate(value, options) {
|
|
|
74
98
|
const checkSum = generateCheckSum(value).toString();
|
|
75
99
|
return (options === null || options === void 0 ? void 0 : options.checkSumOnly) ? checkSum : value.concat(checkSum);
|
|
76
100
|
}
|
|
77
|
-
exports.generate = generate;
|
|
78
101
|
/**
|
|
79
102
|
* Determine if the Luhn checksum for a given number is correct
|
|
80
103
|
*
|
|
@@ -89,7 +112,6 @@ function validate(value) {
|
|
|
89
112
|
const valueWithoutCheckSum = value.substring(0, value.length - 1);
|
|
90
113
|
return value === generate(valueWithoutCheckSum);
|
|
91
114
|
}
|
|
92
|
-
exports.validate = validate;
|
|
93
115
|
/**
|
|
94
116
|
* Generate a random number with valid Luhn checksum
|
|
95
117
|
*
|
|
@@ -100,10 +122,10 @@ function random(length) {
|
|
|
100
122
|
handleErrors(length);
|
|
101
123
|
const lengthAsInteger = parseInt(length);
|
|
102
124
|
if (lengthAsInteger > 100) {
|
|
103
|
-
throw new Error('
|
|
125
|
+
throw new Error('string must be less than 100 characters');
|
|
104
126
|
}
|
|
105
127
|
if (lengthAsInteger < 2) {
|
|
106
|
-
throw new Error('
|
|
128
|
+
throw new Error('string must be greater than 1');
|
|
107
129
|
}
|
|
108
130
|
const random = Array.from({ length: lengthAsInteger - 1 }, (_, index) => {
|
|
109
131
|
// Ensure the first digit is not zero
|
|
@@ -114,7 +136,6 @@ function random(length) {
|
|
|
114
136
|
}).join('');
|
|
115
137
|
return generate(random);
|
|
116
138
|
}
|
|
117
|
-
exports.random = random;
|
|
118
139
|
/**
|
|
119
140
|
* Calculate and append a Luhn mod-N checksum to a numeric string
|
|
120
141
|
*
|
|
@@ -124,15 +145,14 @@ exports.random = random;
|
|
|
124
145
|
* @returns String containing either the checksum character alone or input with checksum appended
|
|
125
146
|
*/
|
|
126
147
|
function generateModN(value, n, options) {
|
|
127
|
-
handleErrors(value);
|
|
128
148
|
if (n < 1 || n > 36) {
|
|
129
149
|
throw new Error('n must be between 1 and 36');
|
|
130
150
|
}
|
|
151
|
+
handleModNErrors(value, n);
|
|
131
152
|
const checkSum = checksumModN(value, n);
|
|
132
153
|
const checkChar = CODE_POINTS[checkSum];
|
|
133
154
|
return (options === null || options === void 0 ? void 0 : options.checkSumOnly) ? checkChar : value.concat(checkChar);
|
|
134
155
|
}
|
|
135
|
-
exports.generateModN = generateModN;
|
|
136
156
|
/**
|
|
137
157
|
* Determine if the Luhn mod-N checksum for a given value is correct
|
|
138
158
|
*
|
|
@@ -141,29 +161,23 @@ exports.generateModN = generateModN;
|
|
|
141
161
|
* @returns boolean indicating whether the checksum is valid
|
|
142
162
|
*/
|
|
143
163
|
function validateModN(value, n) {
|
|
144
|
-
if (
|
|
145
|
-
throw new Error(
|
|
146
|
-
}
|
|
147
|
-
if (!value.length) {
|
|
148
|
-
throw new Error('string cannot be empty');
|
|
164
|
+
if (n < 1 || n > 36) {
|
|
165
|
+
throw new Error('n must be between 1 and 36');
|
|
149
166
|
}
|
|
167
|
+
handleModNErrors(value, n);
|
|
150
168
|
if (value.length === 1) {
|
|
151
169
|
throw new Error('string must be longer than 1 character');
|
|
152
170
|
}
|
|
153
|
-
if (n < 1 || n > 36) {
|
|
154
|
-
throw new Error('n must be between 1 and 36');
|
|
155
|
-
}
|
|
156
171
|
const valueWithoutCheckSum = value.substring(0, value.length - 1);
|
|
157
172
|
return value === generateModN(valueWithoutCheckSum, n);
|
|
158
173
|
}
|
|
159
|
-
exports.validateModN = validateModN;
|
|
160
174
|
/**
|
|
161
175
|
* Convert a character to its code point index (0-9, A-Z, case-insensitive)
|
|
162
176
|
*/
|
|
163
177
|
function charToInt(char) {
|
|
164
178
|
const index = CODE_POINTS.indexOf(char.toUpperCase());
|
|
165
179
|
if (index === -1) {
|
|
166
|
-
throw new Error(`
|
|
180
|
+
throw new Error(`invalid character: <${char}>`);
|
|
167
181
|
}
|
|
168
182
|
return index;
|
|
169
183
|
}
|
|
@@ -175,15 +189,10 @@ function charToInt(char) {
|
|
|
175
189
|
* @returns Check digit as a number
|
|
176
190
|
*/
|
|
177
191
|
function checksumModN(value, n) {
|
|
178
|
-
if (typeof value !== 'string') {
|
|
179
|
-
throw new Error(`value must be a string - received ${value}`);
|
|
180
|
-
}
|
|
181
|
-
if (!value.length) {
|
|
182
|
-
throw new Error('string cannot be empty');
|
|
183
|
-
}
|
|
184
192
|
if (n < 1 || n > 36) {
|
|
185
193
|
throw new Error('n must be between 1 and 36');
|
|
186
194
|
}
|
|
195
|
+
handleModNErrors(value, n);
|
|
187
196
|
const chars = Array.from(value);
|
|
188
197
|
let factor = 2;
|
|
189
198
|
let sum = 0;
|
|
@@ -195,4 +204,3 @@ function checksumModN(value, n) {
|
|
|
195
204
|
}
|
|
196
205
|
return (n - (sum % n)) % n;
|
|
197
206
|
}
|
|
198
|
-
exports.checksumModN = checksumModN;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jrrembert/luhnjs",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"repository": "https://github.com/jrrembert/luhnjs.git",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"private": false,
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
12
15
|
"scripts": {
|
|
13
16
|
"build": "tsc && node dist/index.js",
|
|
14
17
|
"lint": "eslint .",
|
|
@@ -16,14 +19,15 @@
|
|
|
16
19
|
"prepublishOnly": "yarn lint && yarn test && yarn build"
|
|
17
20
|
},
|
|
18
21
|
"devDependencies": {
|
|
19
|
-
"@types/jest": "^
|
|
22
|
+
"@types/jest": "^30.0.0",
|
|
20
23
|
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
|
21
24
|
"@typescript-eslint/parser": "^5.62.0",
|
|
22
25
|
"eslint": "^8.28.0",
|
|
23
|
-
"jest": "^
|
|
26
|
+
"jest": "^30.0.0",
|
|
27
|
+
"semantic-release": "^25.0.0",
|
|
24
28
|
"ts-jest": "^29.4.6",
|
|
25
29
|
"ts-node": "^10.9.2",
|
|
26
|
-
"
|
|
27
|
-
"typescript": "^
|
|
30
|
+
"tslib": "^2.8.1",
|
|
31
|
+
"typescript": "^5.8.2"
|
|
28
32
|
}
|
|
29
33
|
}
|