1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7const EXPORTED_SYMBOLS = ["UnitConverterSimple"];
8
9// NOTE: This units table need to be localized upon supporting multi locales
10//       since it supports en-US only.
11//       e.g. Should support plugada or funty as well for pound.
12const UNITS_GROUPS = [
13  {
14    // Angle
15    degree: 1,
16    deg: "degree",
17    d: "degree",
18    radian: Math.PI / 180.0,
19    rad: "radian",
20    r: "radian",
21    gradian: 1 / 0.9,
22    grad: "gradian",
23    g: "gradian",
24    minute: 60,
25    min: "minute",
26    m: "minute",
27    second: 3600,
28    sec: "second",
29    s: "second",
30    sign: 1 / 30.0,
31    mil: 1 / 0.05625,
32    revolution: 1 / 360.0,
33    circle: 1 / 360.0,
34    turn: 1 / 360.0,
35    quadrant: 1 / 90.0,
36    rightangle: 1 / 90.0,
37    sextant: 1 / 60.0,
38  },
39  {
40    // Force
41    newton: 1,
42    n: "newton",
43    kilonewton: 0.001,
44    kn: "kilonewton",
45    "gram-force": 101.9716213,
46    gf: "gram-force",
47    "kilogram-force": 0.1019716213,
48    kgf: "kilogram-force",
49    "ton-force": 0.0001019716213,
50    tf: "ton-force",
51    exanewton: 1.0e-18,
52    en: "exanewton",
53    petanewton: 1.0e-15,
54    PN: "petanewton",
55    Pn: "petanewton",
56    teranewton: 1.0e-12,
57    tn: "teranewton",
58    giganewton: 1.0e-9,
59    gn: "giganewton",
60    meganewton: 0.000001,
61    MN: "meganewton",
62    Mn: "meganewton",
63    hectonewton: 0.01,
64    hn: "hectonewton",
65    dekanewton: 0.1,
66    dan: "dekanewton",
67    decinewton: 10,
68    dn: "decinewton",
69    centinewton: 100,
70    cn: "centinewton",
71    millinewton: 1000,
72    mn: "millinewton",
73    micronewton: 1000000,
74    µn: "micronewton",
75    nanonewton: 1000000000,
76    nn: "nanonewton",
77    piconewton: 1000000000000,
78    pn: "piconewton",
79    femtonewton: 1000000000000000,
80    fn: "femtonewton",
81    attonewton: 1000000000000000000,
82    an: "attonewton",
83    dyne: 100000,
84    dyn: "dyne",
85    "joule/meter": 1,
86    "j/m": "joule/meter",
87    "joule/centimeter": 100,
88    "j/cm": "joule/centimeter",
89    "ton-force-short": 0.0001124045,
90    short: "ton-force-short",
91    "ton-force-long": 0.0001003611,
92    tonf: "ton-force-long",
93    "kip-force": 0.0002248089,
94    kipf: "kip-force",
95    "pound-force": 0.2248089431,
96    lbf: "pound-force",
97    "ounce-force": 3.5969430896,
98    ozf: "ounce-force",
99    poundal: 7.2330138512,
100    pdl: "poundal",
101    pond: 101.9716213,
102    p: "pond",
103    kilopond: 0.1019716213,
104    kp: "kilopond",
105  },
106  {
107    // Length
108    meter: 1,
109    m: "meter",
110    nanometer: 1000000000,
111    micrometer: 1000000,
112    millimeter: 1000,
113    mm: "millimeter",
114    centimeter: 100,
115    cm: "centimeter",
116    kilometer: 0.001,
117    km: "kilometer",
118    mile: 0.0006213689,
119    mi: "mile",
120    yard: 1.0936132983,
121    yd: "yard",
122    foot: 3.280839895,
123    feet: "foot",
124    ft: "foot",
125    inch: 39.37007874,
126    inches: "inch",
127    in: "inch",
128  },
129  {
130    // Mass
131    kilogram: 1,
132    kg: "kilogram",
133    gram: 1000,
134    g: "gram",
135    milligram: 1000000,
136    mg: "milligram",
137    ton: 0.001,
138    t: "ton",
139    longton: 0.0009842073,
140    "l.t.": "longton",
141    "l/t": "longton",
142    shortton: 0.0011023122,
143    "s.t.": "shortton",
144    "s/t": "shortton",
145    pound: 2.2046244202,
146    lbs: "pound",
147    lb: "pound",
148    ounce: 35.273990723,
149    oz: "ounce",
150    carat: 5000,
151    ffd: 5000,
152  },
153];
154
155// There are some units that will be same in lower case in same unit group.
156// e.g. Mn: meganewton and mn: millinewton on force group.
157// Handle them as case-sensitive.
158const CASE_SENSITIVE_UNITS = ["PN", "Pn", "MN", "Mn"];
159
160const NUMBER_REGEX = "\\d+(?:\\.\\d+)?\\s*";
161const UNIT_REGEX = "[A-Za-zµ0-9_./-]+";
162
163// NOTE: This regex need to be localized upon supporting multi locales
164//       since it supports en-US input format only.
165const QUERY_REGEX = new RegExp(
166  `^(${NUMBER_REGEX})(${UNIT_REGEX})(?:\\s+in\\s+|\\s+to\\s+|\\s*=\\s*)(${UNIT_REGEX})`,
167  "i"
168);
169
170const DECIMAL_PRECISION = 10;
171
172/**
173 * This module converts simple unit such as angle and length.
174 */
175class UnitConverterSimple {
176  /**
177   * Convert the given search string.
178   *
179   * @param {string} searchString
180   * @returns {string} conversion result.
181   */
182  convert(searchString) {
183    const regexResult = QUERY_REGEX.exec(searchString);
184    if (!regexResult) {
185      return null;
186    }
187
188    const target = findUnitGroup(regexResult[2], regexResult[3]);
189
190    if (!target) {
191      return null;
192    }
193
194    const { group, inputUnit, outputUnit } = target;
195    const inputNumber = Number(regexResult[1]);
196    const outputNumber = parseFloat(
197      ((inputNumber / group[inputUnit]) * group[outputUnit]).toPrecision(
198        DECIMAL_PRECISION
199      )
200    );
201
202    try {
203      return new Intl.NumberFormat("en-US", {
204        style: "unit",
205        unit: outputUnit,
206        maximumFractionDigits: DECIMAL_PRECISION,
207      }).format(outputNumber);
208    } catch (e) {}
209
210    return `${outputNumber} ${outputUnit}`;
211  }
212}
213
214/**
215 * Returns the suitable unit group and unit for the given two values.
216 * If could not found suitable unit group, returns null.
217 *
218 * @param {string} inputUnit
219 * @param {string} outputUnit
220 * @returns {object}
221 *   {
222 *     group: one in UNITS_GROUPS,
223 *     inputUnit: input unit,
224 *     outputUnit: output unit,
225 *   }
226 */
227function findUnitGroup(inputUnit, outputUnit) {
228  inputUnit = toSuitableUnit(inputUnit);
229  outputUnit = toSuitableUnit(outputUnit);
230
231  const group = UNITS_GROUPS.find(ug => ug[inputUnit] && ug[outputUnit]);
232
233  if (!group) {
234    return null;
235  }
236
237  const inputValue = group[inputUnit];
238  const outputValue = group[outputUnit];
239
240  return {
241    group,
242    inputUnit: typeof inputValue === "string" ? inputValue : inputUnit,
243    outputUnit: typeof outputValue === "string" ? outputValue : outputUnit,
244  };
245}
246
247function toSuitableUnit(unit) {
248  return CASE_SENSITIVE_UNITS.includes(unit) ? unit : unit.toLowerCase();
249}
250