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 = ["UnitConverterTemperature"];
8
9const ABSOLUTE = ["celsius", "kelvin", "fahrenheit"];
10const ALIAS = ["c", "k", "f"];
11const UNITS = [...ABSOLUTE, ...ALIAS];
12
13const NUMBER_REGEX = "\\d+(?:\\.\\d+)?\\s*";
14const UNIT_REGEX = "\\w+";
15
16// NOTE: This regex need to be localized upon supporting multi locales
17//       since it supports en-US input format only.
18const QUERY_REGEX = new RegExp(
19  `^(${NUMBER_REGEX})(${UNIT_REGEX})(?:\\s+in\\s+|\\s+to\\s+|\\s*=\\s*)(${UNIT_REGEX})`,
20  "i"
21);
22
23const DECIMAL_PRECISION = 10;
24
25/**
26 * This module converts temperature unit.
27 */
28class UnitConverterTemperature {
29  /**
30   * Convert the given search string.
31   *
32   * @param {string} searchString
33   * @returns {string} conversion result.
34   */
35  convert(searchString) {
36    const regexResult = QUERY_REGEX.exec(searchString);
37    if (!regexResult) {
38      return null;
39    }
40
41    const target = findUnits(regexResult[2], regexResult[3]);
42
43    if (!target) {
44      return null;
45    }
46
47    const { inputUnit, outputUnit } = target;
48    const inputNumber = Number(regexResult[1]);
49    const inputChar = inputUnit.charAt(0);
50    const outputChar = outputUnit.charAt(0);
51
52    let outputNumber;
53    if (inputChar === outputChar) {
54      outputNumber = inputNumber;
55    } else {
56      outputNumber = this[`${inputChar}2${outputChar}`](inputNumber);
57    }
58
59    outputNumber = parseFloat(outputNumber.toPrecision(DECIMAL_PRECISION));
60
61    try {
62      return new Intl.NumberFormat("en-US", {
63        style: "unit",
64        unit: outputUnit,
65        maximumFractionDigits: DECIMAL_PRECISION,
66      }).format(outputNumber);
67    } catch (e) {}
68
69    return `${outputNumber} ${outputUnit}`;
70  }
71
72  c2k(t) {
73    return t + 273.15;
74  }
75
76  c2f(t) {
77    return t * 1.8 + 32;
78  }
79
80  k2c(t) {
81    return t - 273.15;
82  }
83
84  k2f(t) {
85    return this.c2f(this.k2c(t));
86  }
87
88  f2c(t) {
89    return (t - 32) / 1.8;
90  }
91
92  f2k(t) {
93    return this.c2k(this.f2c(t));
94  }
95}
96
97/**
98 * Returns the suitable units for the given two values.
99 * If could not found suitable unit, returns null.
100 *
101 * @param {string} inputUnit
102 * @param {string} outputUnit
103 * @returns {object}
104 *   {
105 *     inputUnit: input unit,
106 *     outputUnit: output unit,
107 *   }
108 */
109function findUnits(inputUnit, outputUnit) {
110  inputUnit = inputUnit.toLowerCase();
111  outputUnit = outputUnit.toLowerCase();
112
113  if (!UNITS.includes(inputUnit) || !UNITS.includes(outputUnit)) {
114    return null;
115  }
116
117  return {
118    inputUnit: toAbsoluteUnit(inputUnit),
119    outputUnit: toAbsoluteUnit(outputUnit),
120  };
121}
122
123function toAbsoluteUnit(unit) {
124  if (unit.length !== 1) {
125    return unit;
126  }
127
128  return ABSOLUTE.find(a => a.startsWith(unit));
129}
130