chat/modules/ToLocaleFormat.jsm
author Clemens <clemens@gorilla-computing.de>
Mon, 12 Mar 2018 17:44:46 +0100
changeset 30466 9b22a733b765b9a50133661fdccba4e01c3d91de
parent 30438 3cc1812a10b418c8aac85feb001e332cb56892c9
child 33542 985a367b6423f28e297d4c55d0256e741e91d225
permissions -rw-r--r--
Bug 321434 - Location does not appear in multiday view event box. r/a=philipp MozReview-Commit-ID: 4BbeOscF0ec

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

ChromeUtils.import("resource://gre/modules/Services.jsm");

this.EXPORTED_SYMBOLS = ["ToLocaleFormat"];

// JS implementation of the deprecated Date.toLocaleFormat.
// aFormat follows strftime syntax,
// http://pubs.opengroup.org/onlinepubs/007908799/xsh/strftime.html
function ToLocaleFormat(aFormat, aDate) {
  function Day(t) {
    return Math.floor(t.valueOf() / 86400000);
  }
  function DayFromYear(y) {
    return 365 * (y - 1970) +
           Math.floor((y - 1969) / 4) -
           Math.floor((y - 1901) / 100) +
           Math.floor((y - 1601) / 400);
  }
  function DayWithinYear(t) {
    return Day(t) - DayFromYear(t.getFullYear());
  }
  function weekday(option) {
    return aDate.toLocaleString(undefined, {weekday: option});
  }
  function month(option) {
    return aDate.toLocaleString(undefined, {month: option});
  }
  function hourMinSecTwoDigits() {
    return aDate.toLocaleString(undefined, {
      hour: "2-digit", minute: "2-digit", second: "2-digit"
    });
  }
  function dayPeriod() {
    let dtf = Intl.DateTimeFormat(undefined, {hour: "2-digit"});
    let dayPeriodPart =
      dtf.resolvedOptions().hour12 &&
      dtf.formatToParts(aDate).find(part => part.type === "dayPeriod");
    return dayPeriodPart ? dayPeriodPart.value : "";
  }
  function weekNumber(weekStart, t = aDate) {
    let day = t.getDay();
    if (weekStart) {
      day = (day || 7) - weekStart;
    }
    return Math.max(Math.floor((DayWithinYear(t) + 7 - day) / 7), 0);
  }
  function weekNumberISO(t = aDate) {
    let thisWeek = weekNumber(1, t);
    let firstDayOfYear =
      (new Date(t.getFullYear(), 0, 1).getDay() || 7) - 1;
    if (thisWeek === 0 && firstDayOfYear >= 4)
      return weekNumberISO(new Date(t.getFullYear() - 1, 11, 31));
    if (t.getMonth() === 11 &&
        (t.getDate() - ((t.getDay() || 7) - 1)) >= 29)
      return 1;
    return thisWeek + (firstDayOfYear > 0 && firstDayOfYear < 4);
  }
  function weekYearISO() {
    let thisWeek = weekNumber(1, aDate);
    let firstDayOfYear =
      (new Date(aDate.getFullYear(), 0, 1).getDay() || 7) - 1;
    if (thisWeek === 0 && firstDayOfYear >= 4)
      return aDate.getFullYear() - 1;
    if (aDate.getMonth() === 11 &&
        (aDate.getDate() - ((aDate.getDay() || 7) - 1)) >= 29)
      return aDate.getFullYear() + 1;
    return aDate.getFullYear();
  }
  function timeZoneOffset() {
    let offset = aDate.getTimezoneOffset();
    let tzoff =
      Math.floor(Math.abs(offset) / 60) * 100 + Math.abs(offset) % 60;
    return (offset < 0 ? "+" : "-") + String(tzoff).padStart(4, "0");
  }
  function timeZone() {
    let dtf = Intl.DateTimeFormat(undefined, {timeZoneName: "short"});
    let timeZoneNamePart = dtf.formatToParts(aDate)
                              .find(part => part.type === "timeZoneName");
    return timeZoneNamePart ? timeZoneNamePart.value : "";
  }

  let dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
    dateStyle: "full", timeStyle: "long"
  });
  let dateFormatter = new Services.intl.DateTimeFormat(undefined, {
    dateStyle: "full"
  });
  let timeFormatter = new Services.intl.DateTimeFormat(undefined, {
    timeStyle: "long"
  });

  let formatFunctions = {
    a: () => weekday("short"),
    A: () => weekday("long"),
    b: () => month("short"),
    B: () => month("long"),
    c: () => dateTimeFormatter.format(aDate),
    C: () => String(Math.trunc(aDate.getFullYear() / 100)),
    d: () => String(aDate.getDate()),
    D: () => ToLocaleFormat("%m/%d/%y", aDate),
    e: () => String(aDate.getDate()),
    F: () => ToLocaleFormat("%Y-%m-%d", aDate),
    g: () => String(weekYearISO() % 100),
    G: () => String(weekYearISO()),
    h: () => month("short"),
    H: () => String(aDate.getHours()),
    I: () => String(aDate.getHours() % 12 || 12),
    j: () => String(DayWithinYear(aDate) + 1),
    k: () => String(aDate.getHours()),
    l: () => String(aDate.getHours() % 12 || 12),
    m: () => String(aDate.getMonth() + 1),
    M: () => String(aDate.getMinutes()),
    n: () => "\n",
    p: () => dayPeriod().toLocaleUpperCase(),
    P: () => dayPeriod().toLocaleLowerCase(),
    r: () => hourMinSecTwoDigits(),
    R: () => ToLocaleFormat("%H:%M", aDate),
    s: () => String(Math.trunc(aDate.getTime() / 1000)),
    S: () => String(aDate.getSeconds()),
    t: () => "\t",
    T: () => ToLocaleFormat("%H:%M:%S", aDate),
    u: () => String(aDate.getDay() || 7),
    U: () => String(weekNumber(0)),
    V: () => String(weekNumberISO()),
    w: () => String(aDate.getDay()),
    W: () => String(weekNumber(1)),
    x: () => dateFormatter.format(aDate),
    X: () => timeFormatter.format(aDate),
    y: () => String(aDate.getFullYear() % 100),
    Y: () => String(aDate.getFullYear()),
    z: () => timeZoneOffset(),
    Z: () => timeZone(),
    "%": () => "%",
  };
  let padding = {
    C: {fill: "0", width: 2},
    d: {fill: "0", width: 2},
    e: {fill: " ", width: 2},
    g: {fill: "0", width: 2},
    H: {fill: "0", width: 2},
    I: {fill: "0", width: 2},
    j: {fill: "0", width: 3},
    k: {fill: " ", width: 2},
    l: {fill: " ", width: 2},
    m: {fill: "0", width: 2},
    M: {fill: "0", width: 2},
    S: {fill: "0", width: 2},
    U: {fill: "0", width: 2},
    V: {fill: "0", width: 2},
    W: {fill: "0", width: 2},
    y: {fill: "0", width: 2},
  };

  // Modified conversion specifiers E and O are ignored.
  let specifiers = Object.keys(formatFunctions).join("");
  let pattern =
    RegExp(`%#?(\\^)?([0_-]\\d*)?(?:[EO])?([${specifiers}])`, "g");

  return aFormat.replace(pattern,
    (matched, upperCaseFlag, fillWidthFlags, specifier) => {
      let result = formatFunctions[specifier]();
      if (upperCaseFlag)
        result = result.toLocaleUpperCase();
      let fill = (specifier in padding) ? padding[specifier].fill : "";
      let width = (specifier in padding) ? padding[specifier].width : 0;
      if (fillWidthFlags) {
        let newFill = fillWidthFlags[0];
        let newWidth = fillWidthFlags.match(/\d+/);
        if (newFill === "-" && newWidth === null)
          fill = "";
        else {
          fill = newFill === "0" ? "0" : " ";
          width = newWidth !== null ? Number(newWidth) : width;
        }
      }
      return result.padStart(width, fill);
    });
}