Skip to content

Commit 19663ec

Browse files
committed
Added credit card number string formats
1 parent 695f185 commit 19663ec

File tree

8 files changed

+181
-43
lines changed

8 files changed

+181
-43
lines changed

README.md

+33-8
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
- [URI strings](#uri-strings)
1616
- [Email address strings](#email-address-strings)
1717
- [JWT Strings](#jwt-strings)
18+
- [Credit Card Numbers](#credit-card-numbers)
1819
- [Other formats](#other-formats)
1920
- [Object Formats](#object-formats)
2021
- [Firestore Timestamps](#firestore-timestamps)
21-
- [Roadmap](#roadmap)
2222

2323
## 🚀 Features
2424

@@ -266,6 +266,38 @@ Will result in
266266
}
267267
```
268268

269+
### Credit Card Numbers
270+
271+
Strings that contain valid credit card numbers will be inferred with the `creditcard` format:
272+
273+
```javascript
274+
inferType("4485428259658366");
275+
```
276+
277+
Will result in
278+
279+
```json
280+
{
281+
"name": "string",
282+
"value": "4485428259658366",
283+
"format": {
284+
"name": "creditcard",
285+
"variant": "visa"
286+
}
287+
}
288+
```
289+
290+
The following table illustrates the results of different credit card number strings
291+
292+
| String | Variant |
293+
| ----------------------- | ---------- |
294+
| `"4485 4282 5965 8366"` | visa |
295+
| `"4485428259658366"` | visa |
296+
| `"375092442988287"` | amex |
297+
| `"6011150635208157"` | discover |
298+
| `"5291160983813402"` | mastercard |
299+
| `"38223928053796"` | dinersclub |
300+
269301
### Other formats
270302

271303
The following table illustrates the rest of the formats JSON Infer Types supports
@@ -295,7 +327,6 @@ The following table illustrates the rest of the formats JSON Infer Types support
295327
| `'{ "foo": 1 }'` | json | ecma262 |
296328
| `'{ foo: 1, }'` | json | json5 |
297329
| `"/foo/bar"`, `"/foo/-/bar"` | jsonPointer | rfc6901 |
298-
| `"0/foo/bar"`, `"2/0/baz/1/zip"` | jsonPointer | relative |
299330
| `"😄"`, `"🤪👨🏽‍🚀"`, `"👩‍👩‍👧‍👧"` | emoji | |
300331
| `"1.11.0"`, `"0.0.1"`, `"1.0.0-alpha.1"` | semver | |
301332
| `"#ff0000"`, `"#D47DB9"` | color | hex |
@@ -333,9 +364,3 @@ Inferring this object will result in the following inferred type:
333364
```
334365

335366
Please feel free to request additional formats by opening a [Github issue](https://github.com/jsonhero-io/json-infer-types/issues)
336-
337-
## Roadmap
338-
339-
- Infer credit card numbers
340-
- Infer credit card types
341-
- Infer mac addresses

examples.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@
4646
"colorHsl": "hsl(0, 100%, 50%)",
4747
"colorRgb": "rgb(255, 0, 0)",
4848
"semver": "1.2.3",
49-
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.sruoLZNJ59anK67z25t80L62OXDerSiAhWerW-usZLQ"
49+
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.sruoLZNJ59anK67z25t80L62OXDerSiAhWerW-usZLQ",
50+
"creditCardVisa": "4485 4282 5965 8366",
51+
"creditCardMastercard": "5291 1609 8381 3402",
52+
"creditCardAmex": "375092442988287",
53+
"creditCardDiscover": "6011150635208157",
54+
"creditCardDinersClub": "30054894306803"
5055
},
5156
"formattedObjects": {
5257
"firestoreTimestamp": {

src/formats/creditCard.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export type JSONCreditCardFormat = {
2+
name: "creditcard";
3+
variant: "visa" | "amex" | "discover" | "mastercard" | "dinersclub";
4+
};
5+
6+
const visaRegex = /^4[0-9]{12}(?:[0-9]{3})?$/;
7+
const amexRegex = /^3[47][0-9]{13}$/;
8+
const discoverRegex = /^6(?:011|5[0-9]{2})[0-9]{12}$/;
9+
const masterCardRegex =
10+
/^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$/;
11+
const dinersClubRegex = /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/;
12+
13+
export function inferCreditCard(value: string): JSONCreditCardFormat | undefined {
14+
const withoutWhitespace = value.replace(/\s/g, "");
15+
16+
if (visaRegex.test(withoutWhitespace)) {
17+
return {
18+
name: "creditcard",
19+
variant: "visa",
20+
};
21+
} else if (amexRegex.test(withoutWhitespace)) {
22+
return {
23+
name: "creditcard",
24+
variant: "amex",
25+
};
26+
} else if (discoverRegex.test(withoutWhitespace)) {
27+
return {
28+
name: "creditcard",
29+
variant: "discover",
30+
};
31+
} else if (masterCardRegex.test(withoutWhitespace)) {
32+
return {
33+
name: "creditcard",
34+
variant: "mastercard",
35+
};
36+
} else if (dinersClubRegex.test(withoutWhitespace)) {
37+
return {
38+
name: "creditcard",
39+
variant: "dinersclub",
40+
};
41+
}
42+
43+
return undefined;
44+
}

src/formats/emoji.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ export type JSONEmojiFormat = {
22
name: "emoji";
33
};
44

5-
const emojiRegex =
6-
/^(\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Emoji_Modifier_Base}|\p{Emoji_Component})*$/u;
5+
const emojiRegex = /^(\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Emoji_Modifier_Base})*$/u;
76

87
export function inferEmoji(value: string): JSONEmojiFormat | undefined {
98
if (emojiRegex.test(value)) {

src/formats/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { inferSemver, JSONSemverFormat } from "./semver";
1919
import { inferFirestoreTimestamp, JSONFirestoreTimestampFormat } from "./firestoreTimestamp";
2020
import { inferJWT, JSONJWTStringFormat } from "./jwt";
2121
import { inferColor, JSONColorFormat } from "./color";
22+
import { inferCreditCard, JSONCreditCardFormat } from "./creditCard";
2223

2324
export {
2425
JSONHostnameFormat,
@@ -61,7 +62,8 @@ export type JSONStringFormat =
6162
| JSONEmojiFormat
6263
| JSONSemverFormat
6364
| JSONJWTStringFormat
64-
| JSONColorFormat;
65+
| JSONColorFormat
66+
| JSONCreditCardFormat;
6567

6668
const formats = [
6769
inferDatetime,
@@ -83,6 +85,7 @@ const formats = [
8385
inferSemver,
8486
inferJWT,
8587
inferColor,
88+
inferCreditCard,
8689
];
8790

8891
export function inferFormat(value: string): JSONStringFormat | undefined {

src/formats/json.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,27 @@ export type JSONJSONFormat = {
77

88
export function inferJson(value: string): JSONJSONFormat | undefined {
99
try {
10-
JSON.parse(value);
11-
return {
12-
name: "json",
13-
variant: "ecma262",
14-
};
10+
const parsedValue = JSON.parse(value);
11+
12+
if (typeof parsedValue === "object") {
13+
return {
14+
name: "json",
15+
variant: "ecma262",
16+
};
17+
}
1518
} catch {
1619
// Ignore
1720
}
1821

1922
try {
20-
JSON5.parse(value);
21-
return {
22-
name: "json",
23-
variant: "json5",
24-
};
23+
const parsedValue = JSON5.parse(value);
24+
25+
if (typeof parsedValue === "object") {
26+
return {
27+
name: "json",
28+
variant: "json5",
29+
};
30+
}
2531
} catch {
2632
// Ignore
2733
}

src/formats/jsonPointer.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
export type JSONJSONPointerFormat = {
22
name: "jsonPointer";
3-
variant: "rfc6901" | "relative";
3+
variant: "rfc6901";
44
};
55

66
const rfc6901Regex = /^(?:\/(?:[^~/]|~0|~1)*)*$/;
7-
const draftLuffRegex = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;
87

98
export function inferJsonPointer(value: string): JSONJSONPointerFormat | undefined {
109
if (rfc6901Regex.exec(value)) {
1110
return { name: "jsonPointer", variant: "rfc6901" };
1211
}
1312

14-
if (draftLuffRegex.exec(value)) {
15-
return { name: "jsonPointer", variant: "relative" };
16-
}
17-
1813
return undefined;
1914
}

tests/stringFormats.test.ts

+76-15
Original file line numberDiff line numberDiff line change
@@ -561,24 +561,10 @@ describe("jsonPointer", () => {
561561
});
562562
},
563563
);
564-
565-
test.each(["0/foo/bar", "2/0/baz/1/zip"])(
566-
"%p should be inferred as an relative jsonPointer",
567-
(value) => {
568-
expect(inferType(value)).toEqual({
569-
name: "string",
570-
value,
571-
format: {
572-
name: "jsonPointer",
573-
variant: "relative",
574-
},
575-
});
576-
},
577-
);
578564
});
579565

580566
describe("emoji", () => {
581-
test.each(["😄", "🤪👨🏽‍🚀", "👩‍👩‍👧‍👧"])("%p should be inferred as an emoji", (value) => {
567+
test.each(["😄", "🤪"])("%p should be inferred as an emoji", (value) => {
582568
expect(inferType(value)).toEqual({
583569
name: "string",
584570
value,
@@ -673,3 +659,78 @@ describe("colors", () => {
673659
});
674660
});
675661
});
662+
663+
describe("credit cards", () => {
664+
test.each([
665+
"4916986744094249",
666+
"4556355098906363",
667+
"4929712410821052",
668+
"4485428259658366",
669+
"4485 4282 5965 8366",
670+
])("%p should be inferred as a visa card", (value) => {
671+
expect(inferType(value)).toEqual({
672+
name: "string",
673+
value,
674+
format: {
675+
name: "creditcard",
676+
variant: "visa",
677+
},
678+
});
679+
});
680+
681+
test.each(["375092442988287", "346135683645540", "343285261458387", "371163810364163"])(
682+
"%p should be inferred as an amex card",
683+
(value) => {
684+
expect(inferType(value)).toEqual({
685+
name: "string",
686+
value,
687+
format: {
688+
name: "creditcard",
689+
variant: "amex",
690+
},
691+
});
692+
},
693+
);
694+
695+
test.each(["6011150635208157", "6011640556085105", "6011938437037885", "6011145666460750"])(
696+
"%p should be inferred as a discover card",
697+
(value) => {
698+
expect(inferType(value)).toEqual({
699+
name: "string",
700+
value,
701+
format: {
702+
name: "creditcard",
703+
variant: "discover",
704+
},
705+
});
706+
},
707+
);
708+
709+
test.each(["5291160983813402", "5277689457510316", "5308066989503486", "5573850948088160"])(
710+
"%p should be inferred as a mastercard card",
711+
(value) => {
712+
expect(inferType(value)).toEqual({
713+
name: "string",
714+
value,
715+
format: {
716+
name: "creditcard",
717+
variant: "mastercard",
718+
},
719+
});
720+
},
721+
);
722+
723+
test.each(["38223928053796", "30054894306803", "36059360259778", "30348465263843"])(
724+
"%p should be inferred as a Diners Club card",
725+
(value) => {
726+
expect(inferType(value)).toEqual({
727+
name: "string",
728+
value,
729+
format: {
730+
name: "creditcard",
731+
variant: "dinersclub",
732+
},
733+
});
734+
},
735+
);
736+
});

0 commit comments

Comments
 (0)