Skip to content

Commit b83334a

Browse files
authored
Merge pull request #6 from skeate/patterns
feat(patterns): transfer patterns from schemata-ts
2 parents 38fd04c + 04b69ee commit b83334a

24 files changed

+1465
-0
lines changed

src/base.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,21 @@ export const between: (
178178
max,
179179
})
180180

181+
/**
182+
* Repeat an `Atom` at most some number of times. For example, `atMost(3)(char('a'))`
183+
* represents ``, `a`, and `aaa` but not `aaaa`
184+
*
185+
* @since 1.1.0
186+
*/
187+
export const atMost: (min: number) => (atom: Atom) => QuantifiedAtom =
188+
(max) => (atom) => ({
189+
tag: 'quantifiedAtom',
190+
atom,
191+
kind: 'between',
192+
min: 0,
193+
max,
194+
})
195+
181196
/**
182197
* Create a disjunction of two patterns. In regular expression terms, this corresponds to `|`.
183198
*

src/patterns/base64.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { pipe } from 'fp-ts/function'
2+
3+
import {
4+
and,
5+
anyNumber,
6+
char,
7+
characterClass,
8+
exactly,
9+
maybe,
10+
sequence,
11+
subgroup,
12+
then,
13+
} from '../base'
14+
import { alnum } from '../character-classes'
15+
import { oneOf } from '../combinators'
16+
import { Pattern } from '../types'
17+
18+
export const base64Character = pipe(alnum, and(characterClass(false, '+', '/')))
19+
20+
/**
21+
* Matches a base64 string, with or without trailing '=' characters. However, if
22+
* they are present, they must be correct (i.e. pad out the string so its length
23+
* is a multiple of 4)
24+
*
25+
* @since 1.0.0
26+
* @category Pattern
27+
*/
28+
export const base64: Pattern = pipe(
29+
base64Character,
30+
exactly(4),
31+
subgroup,
32+
anyNumber(),
33+
then(
34+
maybe(
35+
subgroup(
36+
oneOf(
37+
sequence(exactly(2)(base64Character), exactly(2)(char('='))),
38+
sequence(exactly(3)(base64Character), char('=')),
39+
),
40+
),
41+
),
42+
),
43+
)

src/patterns/base64url.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { pipe } from 'fp-ts/function'
2+
3+
import { and, anyNumber } from '../base'
4+
import { word } from '../character-classes'
5+
import { Pattern } from '../types'
6+
7+
/**
8+
* Matches any
9+
* [base64url](https://datatracker.ietf.org/doc/html/rfc4648#section-5) string.
10+
*
11+
* @since 1.0.0
12+
* @category Pattern
13+
*/
14+
export const base64Url: Pattern = pipe(
15+
word,
16+
and('-'),
17+
anyNumber({ greedy: true }),
18+
)

src/patterns/credit-card.ts

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/**
2+
* NOTE: This pattern can check certain aspects of a credit card number, but not
3+
* all. Specifically, credit card numbers often use a [Luhn
4+
* checksum](https://en.wikipedia.org/wiki/Luhn_algorithm) to verify that the
5+
* number is valid. This pattern does not check that checksum, so it will accept
6+
* some invalid credit card numbers.
7+
*
8+
* If you want Luhn checksum validation, you can use the
9+
* [`schemata-ts`](https://github.com/jacob-alford/schemata-ts) library.
10+
*/
11+
import { pipe } from 'fp-ts/function'
12+
13+
import {
14+
between,
15+
char,
16+
characterClass,
17+
exactString,
18+
exactly,
19+
or,
20+
sequence,
21+
subgroup,
22+
then,
23+
} from '../base'
24+
import { digit } from '../character-classes'
25+
import { oneOf } from '../combinators'
26+
27+
// source: https://en.wikipedia.org/w/index.php?title=Payment_card_number&oldid=1110892430
28+
// afaict the 13-digit variant has not been a thing for years, but maybe there
29+
// are still some valid cards floating around?
30+
// /(^4(\d{12}|\d{15})$)/
31+
const visa = pipe(
32+
char('4'),
33+
then(pipe(exactly(12)(digit), or(exactly(15)(digit)), subgroup)),
34+
)
35+
36+
// source: https://web.archive.org/web/20180514224309/https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf
37+
// /(^(5[1-5]\d{4}|222[1-9]\d{2}|22[3-9]\d{3}|2[3-6]\d{4}|27[01]\d{3}|2720\d{2})\d{10}$)/
38+
const mastercard = pipe(
39+
subgroup(
40+
pipe(
41+
sequence(char('5'), characterClass(false, ['1', '5']), exactly(4)(digit)),
42+
or(
43+
sequence(
44+
exactString('222'),
45+
characterClass(false, ['1', '9']),
46+
exactly(2)(digit),
47+
),
48+
),
49+
or(
50+
sequence(
51+
exactString('22'),
52+
characterClass(false, ['3', '9']),
53+
exactly(3)(digit),
54+
),
55+
),
56+
or(
57+
sequence(
58+
exactString('2'),
59+
characterClass(false, ['3', '6']),
60+
exactly(4)(digit),
61+
),
62+
),
63+
or(
64+
sequence(
65+
exactString('27'),
66+
characterClass(false, '0', '1'),
67+
exactly(3)(digit),
68+
),
69+
),
70+
or(sequence(exactString('2720'), exactly(2)(digit))),
71+
),
72+
),
73+
then(exactly(10)(digit)),
74+
)
75+
76+
// source: https://web.archive.org/web/20210504163517/https://www.americanexpress.com/content/dam/amex/hk/en/staticassets/merchant/pdf/support-and-services/useful-information-and-downloads/GuidetoCheckingCardFaces.pdf
77+
// /(^3[47]\d{13}$)/
78+
const amex = sequence(
79+
char('3'),
80+
characterClass(false, '4', '7'),
81+
exactly(13)(digit),
82+
)
83+
84+
// US/Canada DCI cards will match as Mastercard (source: https://web.archive.org/web/20081204135437/http://www.mastercard.com/in/merchant/en/solutions_resources/dinersclub.html)
85+
// Others match regex below (source: https://web.archive.org/web/20170822221741/https://www.discovernetwork.com/downloads/IPP_VAR_Compliance.pdf)
86+
// /^(3(0([0-5]\d{5}|95\d{4})|[89]\d{6})\d{8,11}|36\d{6}\d{6,11})$/
87+
const dinersClub = pipe(
88+
sequence(
89+
char('3'),
90+
subgroup(
91+
pipe(
92+
sequence(
93+
char('0'),
94+
subgroup(
95+
pipe(
96+
sequence(characterClass(false, ['0', '5']), exactly(5)(digit)),
97+
or(sequence(exactString('95'), exactly(4)(digit))),
98+
),
99+
),
100+
),
101+
or(sequence(characterClass(false, '8', '9'), exactly(6)(digit))),
102+
),
103+
),
104+
between(8, 11)(digit),
105+
),
106+
or(sequence(exactString('36'), exactly(6)(digit), between(6, 11)(digit))),
107+
subgroup,
108+
)
109+
110+
// source: https://web.archive.org/web/20170822221741/https://www.discovernetwork.com/downloads/IPP_VAR_Compliance.pdf
111+
// /(^(6011(0[5-9]\d{2}|[2-4]\d{3}|74\d{2}|7[7-9]\d{2}|8[6-9]\d{2}|9\d{3})|64[4-9]\d{5}|650[0-5]\d{4}|65060[1-9]\d{2}|65061[1-9]\d{2}|6506[2-9]\d{3}|650[7-9]\d{4}|65[1-9]\d{5})\d{8,11}$)/,
112+
const discover = pipe(
113+
oneOf(
114+
pipe(
115+
exactString('6011'),
116+
then(
117+
subgroup(
118+
oneOf(
119+
sequence(
120+
char('0'),
121+
characterClass(false, ['5', '9']),
122+
exactly(2)(digit),
123+
),
124+
sequence(characterClass(false, ['2', '4']), exactly(3)(digit)),
125+
sequence(exactString('74'), exactly(2)(digit)),
126+
sequence(
127+
exactString('7'),
128+
characterClass(false, ['7', '9']),
129+
exactly(2)(digit),
130+
),
131+
sequence(
132+
exactString('8'),
133+
characterClass(false, ['6', '9']),
134+
exactly(2)(digit),
135+
),
136+
sequence(exactString('9'), exactly(3)(digit)),
137+
),
138+
),
139+
),
140+
),
141+
sequence(
142+
exactString('64'),
143+
characterClass(false, ['4', '9']),
144+
exactly(5)(digit),
145+
),
146+
sequence(
147+
exactString('650'),
148+
characterClass(false, ['0', '5']),
149+
exactly(4)(digit),
150+
),
151+
sequence(
152+
exactString('65060'),
153+
characterClass(false, ['1', '9']),
154+
exactly(2)(digit),
155+
),
156+
sequence(
157+
exactString('65061'),
158+
characterClass(false, ['1', '9']),
159+
exactly(2)(digit),
160+
),
161+
sequence(
162+
exactString('6506'),
163+
characterClass(false, ['2', '9']),
164+
exactly(3)(digit),
165+
),
166+
sequence(
167+
exactString('650'),
168+
characterClass(false, ['7', '9']),
169+
exactly(4)(digit),
170+
),
171+
sequence(
172+
exactString('65'),
173+
characterClass(false, ['1', '9']),
174+
exactly(5)(digit),
175+
),
176+
),
177+
subgroup,
178+
then(between(8, 11)(digit)),
179+
)
180+
181+
// /^(352[89]\d{4}|35[3-8]\d{5})\d{8,11}$/
182+
const jcb = pipe(
183+
sequence(
184+
exactString('352'),
185+
characterClass(false, '8', '9'),
186+
exactly(4)(digit),
187+
),
188+
or(
189+
sequence(
190+
exactString('35'),
191+
characterClass(false, ['3', '8']),
192+
exactly(5)(digit),
193+
),
194+
),
195+
subgroup,
196+
then(between(8, 11)(digit)),
197+
)
198+
199+
// Rupay
200+
// some are JCB co-branded so will match as JCB above
201+
// for the rest, best source I could find is just wikipedia:
202+
// https://en.wikipedia.org/w/index.php?title=Payment_card_number&oldid=1110892430
203+
// /^((60|65|81|82)\d{14}|508\d{14})$/
204+
const rupay = subgroup(
205+
oneOf(
206+
sequence(
207+
subgroup(
208+
oneOf(
209+
exactString('60'),
210+
exactString('65'),
211+
exactString('81'),
212+
exactString('82'),
213+
),
214+
),
215+
exactly(14)(digit),
216+
),
217+
sequence(exactString('508'), exactly(14)(digit)),
218+
),
219+
)
220+
221+
// /^62(2(12[6-9]\d{2}|1[3-9]\d{3}|[2-8]\d|9[01]\d{3}|92[0-5]\d{2})|[4-6]\d{5}|8[2-8]\d{4})\d{8,11}$/
222+
const unionPay = sequence(
223+
exactString('62'),
224+
subgroup(
225+
oneOf(
226+
sequence(
227+
char('2'),
228+
subgroup(
229+
oneOf(
230+
sequence(
231+
exactString('12'),
232+
characterClass(false, ['6', '9']),
233+
exactly(2)(digit),
234+
),
235+
sequence(
236+
char('1'),
237+
characterClass(false, ['3', '9']),
238+
exactly(3)(digit),
239+
),
240+
sequence(characterClass(false, ['2', '8']), digit),
241+
sequence(
242+
exactString('9'),
243+
characterClass(false, '0', '1'),
244+
exactly(3)(digit),
245+
),
246+
sequence(
247+
exactString('92'),
248+
characterClass(false, ['0', '5']),
249+
exactly(2)(digit),
250+
),
251+
),
252+
),
253+
),
254+
sequence(characterClass(false, ['4', '6']), exactly(5)(digit)),
255+
sequence(
256+
exactString('8'),
257+
characterClass(false, ['2', '8']),
258+
exactly(4)(digit),
259+
),
260+
),
261+
),
262+
between(8, 11)(digit),
263+
)
264+
265+
/**
266+
* @since 1.1.0
267+
* @category Pattern
268+
*/
269+
export const creditCard = oneOf(
270+
visa,
271+
mastercard,
272+
amex,
273+
dinersClub,
274+
discover,
275+
jcb,
276+
rupay,
277+
unionPay,
278+
)

0 commit comments

Comments
 (0)