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
5const { XPCOMUtils } = ChromeUtils.import(
6  "resource://gre/modules/XPCOMUtils.jsm"
7);
8
9ChromeUtils.defineModuleGetter(
10  this,
11  "Services",
12  "resource://gre/modules/Services.jsm"
13);
14
15const EXPORTED_SYMBOLS = ["ToLocaleFormat"];
16
17// JS implementation of the deprecated Date.toLocaleFormat.
18// aFormat follows strftime syntax,
19// http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
20
21function Day(t) {
22  return Math.floor(t.valueOf() / 86400000);
23}
24function DayFromYear(y) {
25  return (
26    365 * (y - 1970) +
27    Math.floor((y - 1969) / 4) -
28    Math.floor((y - 1901) / 100) +
29    Math.floor((y - 1601) / 400)
30  );
31}
32function DayWithinYear(t) {
33  return Day(t) - DayFromYear(t.getFullYear());
34}
35function weekday(aDate, option) {
36  return aDate.toLocaleString(undefined, { weekday: option });
37}
38function month(aDate, option) {
39  return aDate.toLocaleString(undefined, { month: option });
40}
41function hourMinSecTwoDigits(aDate) {
42  return aDate.toLocaleString(undefined, {
43    hour: "2-digit",
44    minute: "2-digit",
45    second: "2-digit",
46  });
47}
48function dayPeriod(aDate) {
49  let dtf = Intl.DateTimeFormat(undefined, { hour: "2-digit" });
50  let dayPeriodPart =
51    dtf.resolvedOptions().hour12 &&
52    dtf.formatToParts(aDate).find(part => part.type === "dayPeriod");
53  return dayPeriodPart ? dayPeriodPart.value : "";
54}
55function weekNumber(aDate, weekStart) {
56  let day = aDate.getDay();
57  if (weekStart) {
58    day = (day || 7) - weekStart;
59  }
60  return Math.max(Math.floor((DayWithinYear(aDate) + 7 - day) / 7), 0);
61}
62function weekNumberISO(t) {
63  let thisWeek = weekNumber(1, t);
64  let firstDayOfYear = (new Date(t.getFullYear(), 0, 1).getDay() || 7) - 1;
65  if (thisWeek === 0 && firstDayOfYear >= 4) {
66    return weekNumberISO(new Date(t.getFullYear() - 1, 11, 31));
67  }
68  if (t.getMonth() === 11 && t.getDate() - ((t.getDay() || 7) - 1) >= 29) {
69    return 1;
70  }
71  return thisWeek + (firstDayOfYear > 0 && firstDayOfYear < 4);
72}
73function weekYearISO(aDate) {
74  let thisWeek = weekNumber(1, aDate);
75  let firstDayOfYear = (new Date(aDate.getFullYear(), 0, 1).getDay() || 7) - 1;
76  if (thisWeek === 0 && firstDayOfYear >= 4) {
77    return aDate.getFullYear() - 1;
78  }
79  if (
80    aDate.getMonth() === 11 &&
81    aDate.getDate() - ((aDate.getDay() || 7) - 1) >= 29
82  ) {
83    return aDate.getFullYear() + 1;
84  }
85  return aDate.getFullYear();
86}
87function timeZoneOffset(aDate) {
88  let offset = aDate.getTimezoneOffset();
89  let tzoff = Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60);
90  return (offset < 0 ? "+" : "-") + String(tzoff).padStart(4, "0");
91}
92function timeZone(aDate) {
93  let dtf = Intl.DateTimeFormat(undefined, { timeZoneName: "short" });
94  let timeZoneNamePart = dtf
95    .formatToParts(aDate)
96    .find(part => part.type === "timeZoneName");
97  return timeZoneNamePart ? timeZoneNamePart.value : "";
98}
99
100XPCOMUtils.defineLazyGetter(
101  this,
102  "dateTimeFormatter",
103  () =>
104    new Services.intl.DateTimeFormat(undefined, {
105      dateStyle: "full",
106      timeStyle: "long",
107    })
108);
109XPCOMUtils.defineLazyGetter(
110  this,
111  "dateFormatter",
112  () =>
113    new Services.intl.DateTimeFormat(undefined, {
114      dateStyle: "full",
115    })
116);
117XPCOMUtils.defineLazyGetter(
118  this,
119  "timeFormatter",
120  () =>
121    new Services.intl.DateTimeFormat(undefined, {
122      timeStyle: "long",
123    })
124);
125
126const formatFunctions = {
127  a: aDate => weekday(aDate, "short"),
128  A: aDate => weekday(aDate, "long"),
129  b: aDate => month(aDate, "short"),
130  B: aDate => month(aDate, "long"),
131  c: aDate => dateTimeFormatter.format(aDate),
132  C: aDate => String(Math.trunc(aDate.getFullYear() / 100)),
133  d: aDate => String(aDate.getDate()),
134  D: aDate => ToLocaleFormat("%m/%d/%y", aDate),
135  e: aDate => String(aDate.getDate()),
136  F: aDate => ToLocaleFormat("%Y-%m-%d", aDate),
137  g: aDate => String(weekYearISO(aDate) % 100),
138  G: aDate => String(weekYearISO(aDate)),
139  h: aDate => month(aDate, "short"),
140  H: aDate => String(aDate.getHours()),
141  I: aDate => String(aDate.getHours() % 12 || 12),
142  j: aDate => String(DayWithinYear(aDate) + 1),
143  k: aDate => String(aDate.getHours()),
144  l: aDate => String(aDate.getHours() % 12 || 12),
145  m: aDate => String(aDate.getMonth() + 1),
146  M: aDate => String(aDate.getMinutes()),
147  n: () => "\n",
148  p: aDate => dayPeriod(aDate).toLocaleUpperCase(),
149  P: aDate => dayPeriod(aDate).toLocaleLowerCase(),
150  r: aDate => hourMinSecTwoDigits(aDate),
151  R: aDate => ToLocaleFormat("%H:%M", aDate),
152  s: aDate => String(Math.trunc(aDate.getTime() / 1000)),
153  S: aDate => String(aDate.getSeconds()),
154  t: () => "\t",
155  T: aDate => ToLocaleFormat("%H:%M:%S", aDate),
156  u: aDate => String(aDate.getDay() || 7),
157  U: aDate => String(weekNumber(aDate, 0)),
158  V: aDate => String(weekNumberISO(aDate)),
159  w: aDate => String(aDate.getDay()),
160  W: aDate => String(weekNumber(aDate, 1)),
161  x: aDate => dateFormatter.format(aDate),
162  X: aDate => timeFormatter.format(aDate),
163  y: aDate => String(aDate.getFullYear() % 100),
164  Y: aDate => String(aDate.getFullYear()),
165  z: aDate => timeZoneOffset(aDate),
166  Z: aDate => timeZone(aDate),
167  "%": () => "%",
168};
169const padding = {
170  C: { fill: "0", width: 2 },
171  d: { fill: "0", width: 2 },
172  e: { fill: " ", width: 2 },
173  g: { fill: "0", width: 2 },
174  H: { fill: "0", width: 2 },
175  I: { fill: "0", width: 2 },
176  j: { fill: "0", width: 3 },
177  k: { fill: " ", width: 2 },
178  l: { fill: " ", width: 2 },
179  m: { fill: "0", width: 2 },
180  M: { fill: "0", width: 2 },
181  S: { fill: "0", width: 2 },
182  U: { fill: "0", width: 2 },
183  V: { fill: "0", width: 2 },
184  W: { fill: "0", width: 2 },
185  y: { fill: "0", width: 2 },
186};
187
188function ToLocaleFormat(aFormat, aDate) {
189  // Modified conversion specifiers E and O are ignored.
190  let specifiers = Object.keys(formatFunctions).join("");
191  let pattern = RegExp(`%#?(\\^)?([0_-]\\d*)?(?:[EO])?([${specifiers}])`, "g");
192
193  return aFormat.replace(
194    pattern,
195    (matched, upperCaseFlag, fillWidthFlags, specifier) => {
196      let result = formatFunctions[specifier](aDate);
197      if (upperCaseFlag) {
198        result = result.toLocaleUpperCase();
199      }
200      let fill = specifier in padding ? padding[specifier].fill : "";
201      let width = specifier in padding ? padding[specifier].width : 0;
202      if (fillWidthFlags) {
203        let newFill = fillWidthFlags[0];
204        let newWidth = fillWidthFlags.match(/\d+/);
205        if (newFill === "-" && newWidth === null) {
206          fill = "";
207        } else {
208          fill = newFill === "0" ? "0" : " ";
209          width = newWidth !== null ? Number(newWidth) : width;
210        }
211      }
212      return result.padStart(width, fill);
213    }
214  );
215}
216