-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathutil.ts
236 lines (203 loc) · 8.31 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import * as amazon from './tracking_number_data/couriers/amazon.json';
import * as dhl from './tracking_number_data/couriers/dhl.json';
import * as fedex from './tracking_number_data/couriers/fedex.json';
import * as ontrac from './tracking_number_data/couriers/ontrac.json';
import * as s10 from './tracking_number_data/couriers/s10.json';
import * as ups from './tracking_number_data/couriers/ups.json';
import * as usps from './tracking_number_data/couriers/usps.json';
import {
is, pipe, split, map, sum, zip, multiply, complement, pickBy, values, prop, join, flip, match, uniq,
identity, ifElse, filter, none, test, flatten, chain, isNil, replace, reduce, reduced
} from 'ramda';
import {
TrackingCourier, TrackingData, SerialData, Additional, Lookup, LookupServiceType, MatchCourier, SerialNumberFormat,
Courier, TrackingNumber
} from './types';
export { amazon, dhl, fedex, ontrac, s10, ups, usps, TrackingCourier, TrackingData, Courier, TrackingNumber };
export const allCouriers: readonly TrackingCourier[] = [amazon, dhl, fedex, ontrac, s10, ups, usps];
const additionalCheck = (match: Partial<SerialData>) => (a: Additional): boolean =>
a.regex_group_name === 'ServiceType'
? a.lookup.some((x: Lookup) => (x as LookupServiceType).matches_regex
? new RegExp((x as LookupServiceType).matches_regex).test(match.groups![a.regex_group_name])
// seems not required to be true? https://github.com/jkeen/tracking_number_data/issues/43
// : a.lookup.some((x: MatchServiceType) => x.matches === match.groups[a.regex_group_name]);
: true
)
: a.regex_group_name === 'CountryCode' || a.regex_group_name === 'ShippingContainerType'
? a.lookup.some(x => (x as MatchCourier).matches === match.groups![a.regex_group_name])
: true;
const matchTrackingData = (trackingNumber: string, regex: string | readonly string[]): Partial<SerialData> => {
const r = is(String, regex)
? regex as string
: (regex as readonly string[]).join('');
const match = new RegExp(`\\b${r}\\b`).exec(trackingNumber.replace(/[^a-zA-Z\d]/g, ''));
return match && {
serial: match.groups!.SerialNumber.replace(/\s/g, ''),
checkDigit: match.groups!.CheckDigit,
groups: match.groups,
} || {};
};
const additional = (t: string, tracking: TrackingData): boolean => tracking.additional
? tracking.additional.every(additionalCheck(matchTrackingData(t, tracking.regex)))
: true;
const dummy = (_serialData: SerialData): boolean => true;
const formatList = (tracking: string): readonly number[] => pipe(
split(''),
map(
(x: string | number) => isNaN(x as number)
? ((x as string).charCodeAt(0) - 3) % 10
: parseInt(x as string)
)
)(tracking);
const toObj = (list: readonly number[]): Record<string, string | number> =>
Object.assign({}, list) as unknown as Record<string, string | number>;
const evenKeys = (_v: number, k: number): boolean => k % 2 === 0;
const oddKeys = complement(evenKeys);
const getSum = (parityFn: (v: number, k: number) => boolean, tracking: readonly number[]): number => pipe<
readonly number[],
Record<string, string | number>,
Record<string, number>,
readonly number[],
number
>(
toObj,
// @ts-ignore Bad Ramda types
pickBy(parityFn),
values,
sum
)(tracking);
const mod10 = ({ serial, checkDigit, checksum }: SerialData): boolean => {
const t = formatList(serial.replace(/[^\da-zA-Z]/g, ''));
const keySum = sum([
getSum(evenKeys, t) * (checksum.evens_multiplier || 1),
getSum(oddKeys, t) * (checksum.odds_multiplier || 1),
]);
return (10 - keySum % 10) % 10 === parseInt(checkDigit);
};
const mod7 = ({ serial, checkDigit }: SerialData): boolean => parseInt(serial) % 7 === parseInt(checkDigit);
const addWeight = (weightings: readonly number[], serial: string): number => sum(
zip(
serial.split('').map(s => parseInt(s)),
weightings || []
).map(x => x.reduce(multiply))
);
const validateS10 = ({ serial, checkDigit }: SerialData): boolean => {
const remainder = addWeight([8, 6, 4, 2, 3, 5, 9, 7], serial) % 11;
const check = remainder === 1
? 0
: remainder === 0
? 5
: 11 - remainder;
return check === parseInt(checkDigit);
};
const sumProductWithWeightingsAndModulo = ({ serial, checkDigit, checksum }: SerialData): boolean =>
addWeight(checksum.weightings!, serial) % checksum.modulo1! % checksum.modulo2! === parseInt(checkDigit);
const validator = ({ validation: { checksum } }: TrackingData): (x: SerialData) => boolean =>
checksum?.name === 'mod10'
? mod10
: checksum?.name === 'sum_product_with_weightings_and_modulo'
? sumProductWithWeightingsAndModulo
: checksum?.name === 'mod7'
? mod7
: checksum?.name === 's10'
? validateS10
: dummy;
const formatSerial = (serial: string, numberFormat: SerialNumberFormat): string =>
numberFormat.prepend_if && new RegExp(numberFormat.prepend_if.matches_regex).test(serial)
? `${numberFormat.prepend_if.content}${serial}`
: serial;
const getSerialData = (
trackingNumber: string,
// eslint-disable-next-line camelcase
{ regex, validation: { serial_number_format, checksum } }: TrackingData
): SerialData | null => {
const trackingData = matchTrackingData(trackingNumber, regex);
return trackingData && trackingData.serial
? {
// eslint-disable-next-line camelcase
serial: serial_number_format
? formatSerial(trackingData.serial, serial_number_format)
: trackingData.serial,
checkDigit: trackingData.checkDigit!,
checksum: checksum!,
}
: null;
};
const toTrackingNumber = (t: TrackingData, c: TrackingCourier, trackingNumber: string): TrackingNumber => ({
name: t.name,
trackingUrl: t.tracking_url || null,
description: t.description || null,
trackingNumber: trackingNumber.replace(/[^a-zA-Z\d]/g, ''),
// @todo add lookups
courier: {
name: c.name,
code: c.courier_code,
},
});
const getTrackingList = (searchText: string) => (trackingData: TrackingData): readonly string[] => pipe<
TrackingData,
string | readonly string[],
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
readonly string[],
readonly string[],
readonly string[]
>(
prop('regex'),
ifElse(
is(String),
identity,
join(''),
),
(r: string) => new RegExp(r, 'g'),
flip(match)(searchText),
map(replace(/[^a-zA-Z\d\n\r]/g, '')),
uniq,
)(trackingData);
const getCourierList = (searchText: string, couriers: readonly TrackingCourier[]): readonly string[] => couriers.map(
pipe<TrackingCourier, readonly TrackingData[], unknown>(
prop('tracking_numbers'),
chain(pipe(getTrackingList(searchText), flatten)),
)
) as readonly string[];
const findTrackingMatches = (searchText: string, couriers: readonly TrackingCourier[]): readonly string[] => pipe<
readonly string[],
readonly string[],
readonly string[],
readonly string[],
readonly string[]
>(
flatten,
uniq,
(a: readonly string[]) => filter((t: string) =>
none(test(new RegExp(`([a-zA-Z0-9 ]+)${t}$`)), a)
// @ts-ignore Bad Dictionary Type
)(a) as readonly string[],
(a: readonly string[]) => filter((t: string) =>
none(test(new RegExp(`^${t}([a-zA-Z0-9 ]+)`)), a)
// @ts-ignore Bad Dictionary Type
)(a) as readonly string[]
)(getCourierList(searchText, couriers));
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const getTrackingInternal = (trackingNumber: string) => reduce(
(prev: unknown, courier: TrackingCourier) => (
prev || reduce((_: TrackingNumber | undefined, tn: TrackingData) => {
const serialData = getSerialData(trackingNumber, tn);
return (serialData && validator(tn)(serialData) && additional(trackingNumber, tn))
? reduced(toTrackingNumber(tn, courier, trackingNumber))
: undefined;
}, undefined, courier.tracking_numbers)
),
undefined
) as (couriers: readonly TrackingCourier[]) => TrackingNumber | undefined;
export const getTracking = (
trackingNumber: string,
couriers: readonly TrackingCourier[] = allCouriers
): TrackingNumber | undefined => (
getTrackingInternal(trackingNumber)(couriers)
);
export const findTracking = (searchText: string, couriers?: readonly TrackingCourier[]): readonly TrackingNumber[] =>
findTrackingMatches(searchText, couriers || allCouriers)
.map(t => getTracking(t, couriers || allCouriers))
.filter(complement(isNil)) as readonly TrackingNumber[];