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