@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 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
- - [Continuous Integration](docs/CI.md) - CI/CD workflows and troubleshooting
11
- - [Release and Build Process](docs/RELEASE.md) - Guide for building and publishing releases
12
- - [Specification](docs/SPEC.md) - Cross-language port alignment and API specification
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
@@ -1,6 +1,5 @@
1
- declare class GenerateOptions {
1
+ interface GenerateOptions {
2
2
  checkSumOnly: boolean;
3
- constructor();
4
3
  }
5
4
  /**
6
5
  * Calculate and append Luhn algorithm checksum to a given value
package/dist/src/luhn.js CHANGED
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checksumModN = exports.validateModN = exports.generateModN = exports.random = exports.validate = exports.generate = void 0;
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('length must be less than or equal to 100');
125
+ throw new Error('string must be less than 100 characters');
104
126
  }
105
127
  if (lengthAsInteger < 2) {
106
- throw new Error('length must be greater than or equal to 2');
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 (typeof value !== 'string') {
145
- throw new Error(`value must be a string - received ${value}`);
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(`Invalid character: ${char}`);
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.1",
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": "^29.2.3",
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": "^29.3.1",
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
- "semantic-release": "^24.0.0",
27
- "typescript": "^4.9.3"
30
+ "tslib": "^2.8.1",
31
+ "typescript": "^5.8.2"
28
32
  }
29
33
  }