Bug 1941046 - Part 4: Send a callback request for impression and clicks of MARS Top...
[gecko.git] / toolkit / components / formautofill / shared / AddressParser.sys.mjs
blobe3d4c0e6cde0091bcc6c13d24c2e35864287f29c
1 /* eslint-disable no-useless-concat */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 // NamedCaptureGroup class represents a named capturing group in a regular expression
7 class NamedCaptureGroup {
8   // The named of this capturing group
9   #name = null;
11   // The capturing group
12   #capture = null;
14   // The matched result
15   #match = null;
17   constructor(name, capture) {
18     this.#name = name;
19     this.#capture = capture;
20   }
22   get name() {
23     return this.#name;
24   }
26   get capture() {
27     return this.#capture;
28   }
30   get match() {
31     return this.#match;
32   }
34   // Setter for the matched result based on the match groups
35   setMatch(matchGroups) {
36     this.#match = matchGroups[this.#name];
37   }
40 // Base class for different part of a street address regular expression.
41 // The regular expression is constructed with prefix, pattern, suffix
42 // and separator to extract "value" part.
43 // For examplem, when we write "apt 4." to for floor number, its prefix is `apt`,
44 // suffix is `.` and value to represent apartment number is `4`.
45 class StreetAddressPartRegExp extends NamedCaptureGroup {
46   constructor(name, prefix, pattern, suffix, sep, optional = false) {
47     prefix = prefix ?? "";
48     suffix = suffix ?? "";
49     super(
50       name,
51       `((?:${prefix})(?<${name}>${pattern})(?:${suffix})(?:${sep})+)${
52         optional ? "?" : ""
53       }`
54     );
55   }
58 // A regular expression to match the street number portion of a street address,
59 class StreetNumberRegExp extends StreetAddressPartRegExp {
60   static PREFIX = "((no|°|º|number)(\\.|-|\\s)*)?"; // From chromium source
62   static PATTERN = "\\d+\\w?";
64   // TODO: possible suffix : (th\\.|\\.)?
65   static SUFFIX = null;
67   constructor(sep, optional) {
68     super(
69       StreetNumberRegExp.name,
70       StreetNumberRegExp.PREFIX,
71       StreetNumberRegExp.PATTERN,
72       StreetNumberRegExp.SUFFIX,
73       sep,
74       optional
75     );
76   }
79 // A regular expression to match the street name portion of a street address,
80 class StreetNameRegExp extends StreetAddressPartRegExp {
81   static PREFIX = null;
83   static PATTERN = "(?:[^\\s,]+(?:[^\\S\\r\\n]+[^\\s,]+)*?)"; // From chromium source
85   // TODO: Should we consider suffix like (ave|st)?
86   static SUFFIX = null;
88   constructor(sep, optional) {
89     super(
90       StreetNameRegExp.name,
91       StreetNameRegExp.PREFIX,
92       StreetNameRegExp.PATTERN,
93       StreetNameRegExp.SUFFIX,
94       sep,
95       optional
96     );
97   }
100 // A regular expression to match the apartment number portion of a street address,
101 class ApartmentNumberRegExp extends StreetAddressPartRegExp {
102   static keyword = "apt|apartment|wohnung|apto|-" + "|unit|suite|ste|#|room"; // From chromium source // Firefox specific
103   static PREFIX = `(${ApartmentNumberRegExp.keyword})(\\.|\\s|-)*`;
105   static PATTERN = "\\w*([-|\\/]\\w*)?";
107   static SUFFIX = "(\\.|\\s|-)*(ª)?"; // From chromium source
109   constructor(sep, optional) {
110     super(
111       ApartmentNumberRegExp.name,
112       ApartmentNumberRegExp.PREFIX,
113       ApartmentNumberRegExp.PATTERN,
114       ApartmentNumberRegExp.SUFFIX,
115       sep,
116       optional
117     );
118   }
121 // A regular expression to match the floor number portion of a street address,
122 class FloorNumberRegExp extends StreetAddressPartRegExp {
123   static keyword =
124     "floor|flur|fl|og|obergeschoss|ug|untergeschoss|geschoss|andar|piso|º" + // From chromium source
125     "|level|lvl"; // Firefox specific
126   static PREFIX = `(${FloorNumberRegExp.keyword})?(\\.|\\s|-)*`; // TODO
127   static PATTERN = "\\d{1,3}\\w?";
128   static SUFFIX = `(st|nd|rd|th)?(\\.|\\s|-)*(${FloorNumberRegExp.keyword})?`; // TODO
130   constructor(sep, optional) {
131     super(
132       FloorNumberRegExp.name,
133       FloorNumberRegExp.PREFIX,
134       FloorNumberRegExp.PATTERN,
135       FloorNumberRegExp.SUFFIX,
136       sep,
137       optional
138     );
139   }
143  * Class represents a street address with the following fields:
144  * - street number
145  * - street name
146  * - apartment number
147  * - floor number
148  */
149 export class StructuredStreetAddress {
150   #street_number = null;
151   #street_name = null;
152   #apartment_number = null;
153   #floor_number = null;
155   // If name_first is true, then the street name is given first,
156   // otherwise the street number is given first.
157   constructor(
158     name_first,
159     street_number,
160     street_name,
161     apartment_number,
162     floor_number
163   ) {
164     this.#street_number = name_first
165       ? street_name?.toString()
166       : street_number?.toString();
167     this.#street_name = name_first
168       ? street_number?.toString()
169       : street_name?.toString();
170     this.#apartment_number = apartment_number?.toString();
171     this.#floor_number = floor_number?.toString();
172   }
174   get street_number() {
175     return this.#street_number;
176   }
178   get street_name() {
179     return this.#street_name;
180   }
182   get apartment_number() {
183     return this.#apartment_number;
184   }
186   get floor_number() {
187     return this.#floor_number;
188   }
190   toString() {
191     return `
192       street number: ${this.#street_number}\n
193       street name: ${this.#street_name}\n
194       apartment number: ${this.#apartment_number}\n
195       floor number: ${this.#floor_number}\n
196     `;
197   }
200 export class AddressParser {
201   /**
202    * Parse street address with the following pattern.
203    * street number, street name, apartment number(optional), floor number(optional)
204    * For example, 2 Harrison St #175 floor 2
205    *
206    * @param {string} address The street address to be parsed.
207    * @returns {StructuredStreetAddress}
208    */
209   static parseStreetAddress(address) {
210     if (!address) {
211       return null;
212     }
214     const separator = "(\\s|,|$)";
216     const regexpes = [
217       new StreetNumberRegExp(separator),
218       new StreetNameRegExp(separator),
219       new ApartmentNumberRegExp(separator, true),
220       new FloorNumberRegExp(separator, true),
221     ];
223     if (AddressParser.parse(address, regexpes)) {
224       return new StructuredStreetAddress(
225         false,
226         ...regexpes.map(regexp => regexp.match)
227       );
228     }
230     // Swap the street number and name.
231     const regexpesReverse = [
232       regexpes[1],
233       regexpes[0],
234       regexpes[2],
235       regexpes[3],
236     ];
238     if (AddressParser.parse(address, regexpesReverse)) {
239       return new StructuredStreetAddress(
240         true,
241         ...regexpesReverse.map(regexp => regexp.match)
242       );
243     }
245     return null;
246   }
248   static parse(address, regexpes) {
249     const options = {
250       trim: true,
251       merge_whitespace: true,
252     };
253     address = AddressParser.normalizeString(address, options);
255     const match = address.match(
256       new RegExp(`^(${regexpes.map(regexp => regexp.capture).join("")})$`, "i")
257     );
258     if (!match) {
259       return null;
260     }
262     regexpes.forEach(regexp => regexp.setMatch(match.groups));
263     return regexpes.reduce((acc, current) => {
264       return { ...acc, [current.name]: current.match };
265     }, {});
266   }
268   static normalizeString(s, options) {
269     if (typeof s != "string") {
270       return s;
271     }
273     if (options.ignore_case) {
274       s = s.toLowerCase();
275     }
277     // process punctuation before whitespace because if a punctuation
278     // is replaced with whitespace, we might want to merge it later
279     if (options.remove_punctuation) {
280       s = AddressParser.replacePunctuation(s, "");
281     } else if ("replace_punctuation" in options) {
282       const replace = options.replace_punctuation;
283       s = AddressParser.replacePunctuation(s, replace);
284     }
286     // process whitespace
287     if (options.merge_whitespace) {
288       s = AddressParser.mergeWhitespace(s);
289     } else if (options.remove_whitespace) {
290       s = AddressParser.removeWhitespace(s);
291     }
293     return s.trim();
294   }
296   static replacePunctuation(s, replace) {
297     const regex = /\p{Punctuation}/gu;
298     return s?.replace(regex, replace);
299   }
301   static removePunctuation(s) {
302     return s?.replace(/[.,\/#!$%\^&\*;:{}=\-_~()]/g, "");
303   }
305   static replaceControlCharacters(s) {
306     return s?.replace(/[\t\n\r]/g, " ");
307   }
309   static removeWhitespace(s) {
310     return s?.replace(/[\s]/g, "");
311   }
313   static mergeWhitespace(s) {
314     return s?.replace(/\s{2,}/g, " ");
315   }