├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── constants.ts ├── convert.ts ├── convert_test.ts ├── datetime.ts ├── datetime_test.ts ├── diff.ts ├── docs ├── _config.yml ├── format.md ├── index.md ├── math.md ├── parse.md ├── quick_tour.md ├── timezone.md └── utils.md ├── format.ts ├── format_test.ts ├── local_time.ts ├── locale.ts ├── locale_test.ts ├── mod.ts ├── parse_date.ts ├── parse_date_test.ts ├── timezone.ts ├── timezone_test.ts ├── types.ts ├── utils.ts ├── zoned_time.ts └── zoned_time_test.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: tests (${{ matrix.os }}) 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | fail-fast: true 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: download deno 16 | uses: denoland/setup-deno@main 17 | with: 18 | deno-version: "1.11.0" 19 | 20 | - name: check format 21 | if: matrix.os == 'ubuntu-latest' 22 | run: deno fmt --check 23 | 24 | - name: check linting 25 | if: matrix.os == 'ubuntu-latest' 26 | run: deno lint 27 | 28 | - name: run tests 29 | run: deno test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vim 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "files.trimTrailingWhitespace": true, 4 | "typescript.format.semicolons": "remove", 5 | "typescript.preferences.quoteStyle": "single", 6 | "[javascript]": { 7 | "editor.defaultFormatter": "vscode.typescript-language-features", 8 | "editor.formatOnSave": false 9 | }, 10 | "[typescript]": { 11 | "editor.defaultFormatter": "vscode.typescript-language-features", 12 | "editor.formatOnSave": true, 13 | "editor.codeActionsOnSave": { 14 | "source.organizeImports": true 15 | } 16 | }, 17 | "[typescriptreact]": { 18 | "editor.defaultFormatter": "vscode.typescript-language-features", 19 | "editor.formatOnSave": true, 20 | "editor.codeActionsOnSave": { 21 | "source.organizeImports": true 22 | } 23 | }, 24 | "[rust]": { 25 | "editor.defaultFormatter": "rust-lang.rust", 26 | "editor.formatOnSave": true 27 | }, 28 | "deno.enable": true, 29 | "deno.unstable": true, 30 | "deno.suggest.imports.hosts": { "http://deno.land": true } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Takuro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ptera 2 | 3 | [![ci](https://github.com/Tak-Iwamoto/ptera/actions/workflows/ci.yml/badge.svg)](https://github.com/Tak-Iwamoto/ptera/actions/workflows/ci.yml) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 |

ptera-log

7 | 8 | Ptera is DateTime library for Deno. 9 | 10 | Fully Written in Deno. 11 | 12 | Heavily inspired by the great libraries 13 | [Luxon](https://github.com/moment/luxon), 14 | [Day.js](https://github.com/iamkun/dayjs), 15 | [Moment.js](https://github.com/moment/moment). 16 | 17 | ## Features 18 | 19 | - Immutable, chainable 20 | - Parsing and Formatting 21 | - Timezone and Intl support 22 | 23 | ## Getting Started 24 | 25 | ### API 26 | 27 | ```typescript 28 | import { datetime } from "https://deno.land/x/ptera/mod.ts"; 29 | 30 | datetime("2021-06-30T21:15:30.200"); 31 | 32 | // timezone 33 | datetime().toZonedTime("Asia/Tokyo"); 34 | 35 | // locale 36 | datetime().setLocale("fr"); 37 | 38 | // add, subtract 39 | datetime().add({ year: 1 }); 40 | datetime().subtract({ day: 1 }); 41 | ``` 42 | 43 | ### Documentation 44 | 45 | https://tak-iwamoto.github.io/ptera/ 46 | -------------------------------------------------------------------------------- /constants.ts: -------------------------------------------------------------------------------- 1 | export const MILLISECONDS_IN_DAY = 86400000; 2 | export const MILLISECONDS_IN_HOUR = 3600000; 3 | export const MILLISECONDS_IN_MINUTE = 60000; 4 | -------------------------------------------------------------------------------- /convert.ts: -------------------------------------------------------------------------------- 1 | import { MILLISECONDS_IN_DAY } from "./constants.ts"; 2 | import { adjustedTS } from "./diff.ts"; 3 | import { DateObj } from "./types.ts"; 4 | 5 | export function dateToArray( 6 | dateObj: DateObj, 7 | option?: { jsMonth: boolean }, 8 | ): [ 9 | number, 10 | number, 11 | number, 12 | number, 13 | number, 14 | number, 15 | number, 16 | ] { 17 | const { year, month, day, hour, minute, second, millisecond } = dateObj; 18 | return [ 19 | year, 20 | option?.jsMonth ? month - 1 : month, 21 | day ?? 0, 22 | hour ?? 0, 23 | minute ?? 0, 24 | second ?? 0, 25 | millisecond ?? 0, 26 | ]; 27 | } 28 | 29 | export function dateToTS(dateObj: DateObj): number { 30 | return Date.UTC(...dateToArray(dateObj, { jsMonth: true })); 31 | } 32 | 33 | export function dateToJSDate( 34 | date: DateObj, 35 | ): Date { 36 | return new Date(dateToTS(date)); 37 | } 38 | 39 | export function jsDateToDate(jsDate: Date): DateObj { 40 | return { 41 | year: jsDate.getUTCFullYear(), 42 | month: jsDate.getUTCMonth() + 1, 43 | day: jsDate.getUTCDate(), 44 | hour: jsDate.getUTCHours(), 45 | minute: jsDate.getUTCMinutes(), 46 | second: jsDate.getUTCSeconds(), 47 | millisecond: jsDate.getUTCMilliseconds(), 48 | }; 49 | } 50 | 51 | export function arrayToDate(dateArray: number[]): DateObj { 52 | const result = [NaN, 1, 1, 0, 0, 0, 0]; 53 | for (const [i, v] of dateArray.entries()) { 54 | result[i] = v; 55 | } 56 | const year = result[0]; 57 | const month = result[1]; 58 | const day = result[2]; 59 | const hour = result[3]; 60 | const minute = result[4]; 61 | const second = result[5]; 62 | const millisecond = result[6]; 63 | return { year, month, day, hour, minute, second, millisecond }; 64 | } 65 | 66 | export function tsToDate(ts: number, option?: { isLocal: boolean }): DateObj { 67 | const date = new Date(ts); 68 | if (option && option.isLocal) { 69 | return { 70 | year: date.getFullYear(), 71 | month: date.getMonth() + 1, 72 | day: date.getDate(), 73 | hour: date.getHours(), 74 | minute: date.getMinutes(), 75 | second: date.getSeconds(), 76 | millisecond: date.getMilliseconds(), 77 | }; 78 | } 79 | return { 80 | year: date.getUTCFullYear(), 81 | month: date.getUTCMonth() + 1, 82 | day: date.getUTCDate(), 83 | hour: date.getUTCHours(), 84 | minute: date.getUTCMinutes(), 85 | second: date.getUTCSeconds(), 86 | millisecond: date.getUTCMilliseconds(), 87 | }; 88 | } 89 | 90 | export function dayOfYearToDate(dayOfYear: number, year: number) { 91 | const ts = adjustedTS({ 92 | year, 93 | month: 1, 94 | day: 1, 95 | hour: 0, 96 | minute: 0, 97 | second: 0, 98 | millisecond: 0, 99 | }, { 100 | day: dayOfYear - 1, 101 | }, { positive: true }); 102 | return tsToDate(ts); 103 | } 104 | 105 | export function dateToWeekDay(dateObj: DateObj): number { 106 | const jsDate = dateToJSDate(dateObj); 107 | return jsDate.getUTCDay(); 108 | } 109 | 110 | export function dateToDayOfYear(dateObj: DateObj): number { 111 | const jsDate = dateToJSDate(dateObj); 112 | const utc = jsDate.getTime(); 113 | 114 | jsDate.setUTCMonth(0, 1); 115 | jsDate.setUTCHours(0, 0, 0, 0); 116 | const startOfYear = jsDate.getTime(); 117 | 118 | const diff = utc - startOfYear; 119 | return Math.floor(diff / MILLISECONDS_IN_DAY) + 1; 120 | } 121 | -------------------------------------------------------------------------------- /convert_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 2 | import { arrayToDate } from "./convert.ts"; 3 | 4 | Deno.test("arrayToDate", () => { 5 | const tests = [ 6 | { 7 | input: [2021], 8 | expected: { 9 | year: 2021, 10 | month: 1, 11 | day: 1, 12 | hour: 0, 13 | minute: 0, 14 | second: 0, 15 | millisecond: 0, 16 | }, 17 | }, 18 | { 19 | input: [2021, 12], 20 | expected: { 21 | year: 2021, 22 | month: 12, 23 | day: 1, 24 | hour: 0, 25 | minute: 0, 26 | second: 0, 27 | millisecond: 0, 28 | }, 29 | }, 30 | { 31 | input: [2021, 12, 15], 32 | expected: { 33 | year: 2021, 34 | month: 12, 35 | day: 15, 36 | hour: 0, 37 | minute: 0, 38 | second: 0, 39 | millisecond: 0, 40 | }, 41 | }, 42 | { 43 | input: [2021, 12, 15, 13], 44 | expected: { 45 | year: 2021, 46 | month: 12, 47 | day: 15, 48 | hour: 13, 49 | minute: 0, 50 | second: 0, 51 | millisecond: 0, 52 | }, 53 | }, 54 | { 55 | input: [2021, 12, 15, 13, 30], 56 | expected: { 57 | year: 2021, 58 | month: 12, 59 | day: 15, 60 | hour: 13, 61 | minute: 30, 62 | second: 0, 63 | millisecond: 0, 64 | }, 65 | }, 66 | { 67 | input: [2021, 12, 15, 13, 30, 40], 68 | expected: { 69 | year: 2021, 70 | month: 12, 71 | day: 15, 72 | hour: 13, 73 | minute: 30, 74 | second: 40, 75 | millisecond: 0, 76 | }, 77 | }, 78 | { 79 | input: [2021, 12, 15, 13, 30, 40, 10], 80 | expected: { 81 | year: 2021, 82 | month: 12, 83 | day: 15, 84 | hour: 13, 85 | minute: 30, 86 | second: 40, 87 | millisecond: 10, 88 | }, 89 | }, 90 | { 91 | input: [2021, 12, 15, 13, 30, 40, 10, 30], 92 | expected: { 93 | year: 2021, 94 | month: 12, 95 | day: 15, 96 | hour: 13, 97 | minute: 30, 98 | second: 40, 99 | millisecond: 10, 100 | }, 101 | }, 102 | ]; 103 | 104 | tests.forEach((t) => { 105 | assertEquals(arrayToDate(t.input), t.expected); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /datetime.ts: -------------------------------------------------------------------------------- 1 | import { adjustedTS } from "./diff.ts"; 2 | import { formatDate, formatDateObj } from "./format.ts"; 3 | import { getLocalName } from "./local_time.ts"; 4 | import { tzOffset } from "./timezone.ts"; 5 | import { toOtherZonedTime, zonedTimeToUTC } from "./zoned_time.ts"; 6 | import { Locale } from "./locale.ts"; 7 | import { parseDateStr, parseISO } from "./parse_date.ts"; 8 | import { 9 | arrayToDate, 10 | dateToArray, 11 | dateToDayOfYear, 12 | dateToJSDate, 13 | dateToTS, 14 | dateToWeekDay, 15 | tsToDate, 16 | } from "./convert.ts"; 17 | import { 18 | daysInMonth, 19 | INVALID_DATE, 20 | isLeapYear, 21 | isValidDate, 22 | weeksInWeekYear, 23 | } from "./utils.ts"; 24 | import { DateArray, DateDiff, DateObj, Option, Timezone } from "./types.ts"; 25 | import { 26 | MILLISECONDS_IN_DAY, 27 | MILLISECONDS_IN_HOUR, 28 | MILLISECONDS_IN_MINUTE, 29 | } from "./constants.ts"; 30 | 31 | export type DateArg = Partial | Date | number[] | string | number; 32 | 33 | function isDateObj(arg: DateArg): arg is DateObj { 34 | return (arg as DateObj).year !== undefined; 35 | } 36 | 37 | function isArray(arg: DateArg): arg is number[] { 38 | return (Array.isArray(arg)); 39 | } 40 | 41 | function parseArg(date: DateArg): DateObj { 42 | if (typeof date === "number") { 43 | return tsToDate(date, { isLocal: true }); 44 | } 45 | 46 | if (date instanceof Date) { 47 | return tsToDate(date.getTime(), { isLocal: true }); 48 | } 49 | 50 | if (isDateObj(date)) { 51 | return date; 52 | } 53 | 54 | if (isArray(date)) { 55 | return arrayToDate(date); 56 | } 57 | 58 | if (typeof date === "string") { 59 | const parsed = parseISO(date); 60 | const offset = parsed.offsetMillisec; 61 | if (!offset || offset === 0) return parsed; 62 | 63 | const normalizeSign = offset > 0 ? false : true; 64 | return tsToDate( 65 | adjustedTS(parsed, { millisecond: offset }, { positive: normalizeSign }), 66 | ); 67 | } 68 | 69 | return INVALID_DATE; 70 | } 71 | 72 | export type DateTimeOption = Omit; 73 | 74 | export function latestDateTime(datetimes: DateTime[]) { 75 | return datetimes.reduce((a, b) => 76 | a.toUTC().toMilliseconds() > b.toUTC().toMilliseconds() ? a : b 77 | ); 78 | } 79 | 80 | export function oldestDateTime(datetimes: DateTime[]) { 81 | return datetimes.reduce((a, b) => 82 | a.toUTC().toMilliseconds() < b.toUTC().toMilliseconds() ? a : b 83 | ); 84 | } 85 | 86 | type DiffOption = { showDecimal: boolean }; 87 | 88 | export function diffInMillisec( 89 | baseDate: DateTime, 90 | otherDate: DateTime, 91 | ): number { 92 | return Math.abs( 93 | baseDate.toUTC().toMilliseconds() - otherDate.toUTC().toMilliseconds(), 94 | ); 95 | } 96 | 97 | export function diffInSec(baseDate: DateTime, otherDate: DateTime): number { 98 | return Math.floor(diffInMillisec(baseDate, otherDate) / 1000); 99 | } 100 | 101 | export function diffInMin( 102 | baseDate: DateTime, 103 | otherDate: DateTime, 104 | option: DiffOption = { showDecimal: false }, 105 | ): number { 106 | const diff = diffInMillisec(baseDate, otherDate) / MILLISECONDS_IN_MINUTE; 107 | return option.showDecimal ? diff : Math.floor(diff); 108 | } 109 | 110 | export function diffInHours( 111 | baseDate: DateTime, 112 | otherDate: DateTime, 113 | option: DiffOption = { showDecimal: false }, 114 | ): number { 115 | const diff = diffInMillisec(baseDate, otherDate) / MILLISECONDS_IN_HOUR; 116 | return option.showDecimal ? diff : Math.floor(diff); 117 | } 118 | 119 | export function diffInDays( 120 | baseDate: DateTime, 121 | otherDate: DateTime, 122 | option: DiffOption = { showDecimal: false }, 123 | ): number { 124 | const diff = diffInMillisec(baseDate, otherDate) / MILLISECONDS_IN_DAY; 125 | return option.showDecimal ? diff : Math.floor(diff); 126 | } 127 | 128 | export function datetime(date?: DateArg, option?: DateTimeOption) { 129 | if (date) { 130 | return new DateTime(date, option); 131 | } 132 | return DateTime.now(option); 133 | } 134 | 135 | export class DateTime { 136 | readonly year: number; 137 | readonly month: number; 138 | readonly day: number; 139 | readonly hour: number; 140 | readonly minute: number; 141 | readonly second: number; 142 | readonly millisecond: number; 143 | readonly timezone: Timezone; 144 | readonly valid: boolean; 145 | readonly locale: string; 146 | readonly #localeClass: Locale; 147 | 148 | constructor(date: DateArg, option?: DateTimeOption) { 149 | this.timezone = option?.timezone ?? getLocalName(); 150 | this.locale = option?.locale ?? "en"; 151 | this.#localeClass = new Locale(this.locale); 152 | 153 | const dateObj = parseArg(date); 154 | const { year, month, day, hour, minute, second, millisecond } = dateObj; 155 | this.valid = isValidDate(dateObj); 156 | 157 | if (this.valid) { 158 | this.year = year; 159 | this.month = month; 160 | this.day = day ?? 1; 161 | this.hour = hour ?? 0; 162 | this.minute = minute ?? 0; 163 | this.second = second ?? 0; 164 | this.millisecond = millisecond ?? 0; 165 | } else { 166 | this.year = NaN; 167 | this.month = NaN; 168 | this.day = NaN; 169 | this.hour = NaN; 170 | this.minute = NaN; 171 | this.second = NaN; 172 | this.millisecond = NaN; 173 | } 174 | } 175 | 176 | static now(option?: Option): DateTime { 177 | const localTime = new DateTime(new Date().getTime(), option); 178 | if (option?.timezone) { 179 | return localTime.toZonedTime(option?.timezone).setOption(option); 180 | } 181 | 182 | return localTime; 183 | } 184 | 185 | toLocal(): DateTime { 186 | return this.toZonedTime(getLocalName()); 187 | } 188 | 189 | isValidZone(): boolean { 190 | try { 191 | new Intl.DateTimeFormat("en-US", { timeZone: this.timezone }).format(); 192 | return true; 193 | } catch { 194 | return false; 195 | } 196 | } 197 | 198 | isValid(): boolean { 199 | return isValidDate(this.toDateObj()); 200 | } 201 | 202 | toDateObj(): DateObj { 203 | const { year, month, day, hour, minute, second, millisecond } = this; 204 | return { 205 | year, 206 | month, 207 | day, 208 | hour, 209 | minute, 210 | second, 211 | millisecond, 212 | }; 213 | } 214 | 215 | parse( 216 | dateStr: string, 217 | formatStr: string, 218 | option?: DateTimeOption, 219 | ): DateTime { 220 | const { 221 | year, 222 | month, 223 | day, 224 | hour, 225 | minute, 226 | second, 227 | millisecond, 228 | } = parseDateStr(dateStr, formatStr, { locale: option?.locale ?? "en" }); 229 | 230 | const tz = option?.timezone ?? getLocalName(); 231 | return new DateTime({ 232 | year, 233 | month, 234 | day, 235 | hour, 236 | minute, 237 | second, 238 | millisecond, 239 | }, { ...option, timezone: tz }); 240 | } 241 | 242 | toISO(): string { 243 | const offset = formatDate(this.toDateObj(), "Z", this.#option()); 244 | const tz = this.timezone === "UTC" ? "Z" : offset; 245 | return `${this.toISODate()}T${this.toISOTime()}${tz}`; 246 | } 247 | 248 | toISODate(): string { 249 | return formatDate(this.toDateObj(), "YYYY-MM-dd"); 250 | } 251 | 252 | toISOWeekDate(): string { 253 | return formatDate(this.toDateObj(), "YYYY-'W'WW-w"); 254 | } 255 | 256 | toISOTime(): string { 257 | return formatDate(this.toDateObj(), "HH:mm:ss.S"); 258 | } 259 | 260 | format(formatStr: string) { 261 | return formatDate(this.toDateObj(), formatStr, this.#option()); 262 | } 263 | 264 | toUTC(): DateTime { 265 | const utcDateObj = zonedTimeToUTC( 266 | this.toDateObj(), 267 | this.timezone, 268 | ); 269 | return datetime(utcDateObj, { ...this.#option(), timezone: "UTC" }); 270 | } 271 | 272 | toZonedTime(tz: Timezone): DateTime { 273 | const zonedDateObj = toOtherZonedTime( 274 | this.toDateObj(), 275 | this.timezone, 276 | tz, 277 | ); 278 | return datetime(zonedDateObj, { ...this.#option, timezone: tz }); 279 | } 280 | 281 | toJSDate(): Date { 282 | return dateToJSDate(this.toUTC().toDateObj()); 283 | } 284 | 285 | toArray(): DateArray { 286 | return dateToArray(this.toDateObj()); 287 | } 288 | 289 | toMilliseconds(): number { 290 | return dateToTS(this.toUTC().toDateObj()); 291 | } 292 | 293 | dayOfYear(): number { 294 | return dateToDayOfYear(this.toDateObj()); 295 | } 296 | 297 | weeksInWeekYear(): number { 298 | return weeksInWeekYear(this.year); 299 | } 300 | 301 | weekDay(): number { 302 | return dateToWeekDay(this.toDateObj()); 303 | } 304 | 305 | weekDayShort(): string { 306 | return formatDateObj(this.toDateObj(), "www", this.#option()); 307 | } 308 | 309 | weekDayLong(): string { 310 | return formatDateObj(this.toDateObj(), "wwww", this.#option()); 311 | } 312 | 313 | monthShort(): string { 314 | return formatDateObj(this.toDateObj(), "MMM", this.#option()); 315 | } 316 | 317 | monthLong(): string { 318 | return formatDateObj(this.toDateObj(), "MMMM", this.#option()); 319 | } 320 | 321 | quarter(): number { 322 | return Math.ceil(this.month / 3); 323 | } 324 | 325 | isBefore(otherDate?: DateTime): boolean { 326 | return otherDate 327 | ? this.toMilliseconds() < otherDate.toMilliseconds() 328 | : this.toMilliseconds() < new Date().getTime(); 329 | } 330 | 331 | isAfter(otherDate?: DateTime): boolean { 332 | return otherDate 333 | ? this.toMilliseconds() > otherDate.toMilliseconds() 334 | : this.toMilliseconds() > new Date().getTime(); 335 | } 336 | 337 | isBetween(startDate: DateTime, endDate: DateTime): boolean { 338 | return this.toMilliseconds() >= startDate.toMilliseconds() && 339 | this.toMilliseconds() <= endDate.toMilliseconds(); 340 | } 341 | 342 | isLeapYear(): boolean { 343 | return isLeapYear(this.year); 344 | } 345 | 346 | startOfYear(): DateTime { 347 | return datetime({ 348 | ...this.toDateObj(), 349 | month: 1, 350 | day: 1, 351 | hour: 0, 352 | minute: 0, 353 | second: 0, 354 | millisecond: 0, 355 | }, this.#option()); 356 | } 357 | 358 | startOfMonth(): DateTime { 359 | return datetime({ 360 | ...this.toDateObj(), 361 | day: 1, 362 | hour: 0, 363 | minute: 0, 364 | second: 0, 365 | millisecond: 0, 366 | }, this.#option()); 367 | } 368 | 369 | startOfDay(): DateTime { 370 | return datetime({ 371 | ...this.toDateObj(), 372 | hour: 0, 373 | minute: 0, 374 | second: 0, 375 | millisecond: 0, 376 | }, this.#option()); 377 | } 378 | 379 | startOfHour(): DateTime { 380 | return datetime({ 381 | ...this.toDateObj(), 382 | minute: 0, 383 | second: 0, 384 | millisecond: 0, 385 | }, this.#option()); 386 | } 387 | 388 | startOfMinute(): DateTime { 389 | return datetime({ 390 | ...this.toDateObj(), 391 | second: 0, 392 | millisecond: 0, 393 | }, this.#option()); 394 | } 395 | 396 | startOfSecond(): DateTime { 397 | return datetime({ 398 | ...this.toDateObj(), 399 | millisecond: 0, 400 | }, this.#option()); 401 | } 402 | 403 | startOfQuarter(): DateTime { 404 | return datetime({ 405 | ...this.toDateObj(), 406 | month: 1 + (this.quarter() - 1) * 3, 407 | day: 1, 408 | hour: 0, 409 | minute: 0, 410 | second: 0, 411 | millisecond: 0, 412 | }, this.#option()); 413 | } 414 | 415 | endOfYear(): DateTime { 416 | return datetime({ 417 | ...this.toDateObj(), 418 | month: 12, 419 | day: 31, 420 | hour: 23, 421 | minute: 59, 422 | second: 59, 423 | millisecond: 999, 424 | }, this.#option()); 425 | } 426 | 427 | endOfMonth(): DateTime { 428 | const dateObj = this.toDateObj(); 429 | return datetime({ 430 | ...dateObj, 431 | day: daysInMonth(dateObj.year, dateObj.month), 432 | hour: 23, 433 | minute: 59, 434 | second: 59, 435 | millisecond: 999, 436 | }, this.#option()); 437 | } 438 | 439 | endOfDay(): DateTime { 440 | return datetime({ 441 | ...this.toDateObj(), 442 | hour: 23, 443 | minute: 59, 444 | second: 59, 445 | millisecond: 999, 446 | }, this.#option()); 447 | } 448 | 449 | endOfHour(): DateTime { 450 | return datetime({ 451 | ...this.toDateObj(), 452 | minute: 59, 453 | second: 59, 454 | millisecond: 999, 455 | }, this.#option()); 456 | } 457 | 458 | endOfMinute(): DateTime { 459 | return datetime({ 460 | ...this.toDateObj(), 461 | second: 59, 462 | millisecond: 999, 463 | }, this.#option()); 464 | } 465 | 466 | endOfSecond(): DateTime { 467 | return datetime({ 468 | ...this.toDateObj(), 469 | millisecond: 999, 470 | }, this.#option()); 471 | } 472 | 473 | endOfQuarter(): DateTime { 474 | const month = 3 * this.quarter(); 475 | const dateObj = this.toDateObj(); 476 | return datetime({ 477 | ...dateObj, 478 | month, 479 | day: daysInMonth(dateObj.year, month), 480 | hour: 23, 481 | minute: 59, 482 | second: 59, 483 | millisecond: 999, 484 | }, this.#option()); 485 | } 486 | 487 | add(diff: DateDiff): DateTime { 488 | return datetime( 489 | adjustedTS(this.toUTC().toDateObj(), diff, { positive: true }), 490 | this.#option(), 491 | ); 492 | } 493 | 494 | subtract(diff: DateDiff): DateTime { 495 | return datetime( 496 | adjustedTS(this.toUTC().toDateObj(), diff, { 497 | positive: false, 498 | }), 499 | this.#option(), 500 | ); 501 | } 502 | 503 | offsetMillisec(): number { 504 | if (this.valid) { 505 | return tzOffset( 506 | new Date( 507 | this.year, 508 | this.month - 1, 509 | this.day ?? 0, 510 | this.hour ?? 0, 511 | this.minute ?? 0, 512 | this.second ?? 0, 513 | this.millisecond ?? 0, 514 | ), 515 | this?.timezone ?? "UTC", 516 | ); 517 | } else { 518 | return 0; 519 | } 520 | } 521 | 522 | offsetSec(): number { 523 | return this.offsetMillisec ? this.offsetMillisec() / 1000 : 0; 524 | } 525 | 526 | offsetMin(): number { 527 | return this.offsetMillisec 528 | ? this.offsetMillisec() / MILLISECONDS_IN_MINUTE 529 | : 0; 530 | } 531 | 532 | offsetHour(): number { 533 | return this.offsetMillisec 534 | ? this.offsetMillisec() / MILLISECONDS_IN_HOUR 535 | : 0; 536 | } 537 | 538 | toDateTimeFormat(options?: Intl.DateTimeFormatOptions) { 539 | return this.#localeClass.dtfFormat(this.toJSDate(), options); 540 | } 541 | 542 | toDateTimeFormatParts(options?: Intl.DateTimeFormatOptions) { 543 | return this.#localeClass.dtfFormatToParts(this.toJSDate(), options); 544 | } 545 | 546 | setOption(option: DateTimeOption) { 547 | return datetime(this.toDateObj(), { ...this.#option, ...option }); 548 | } 549 | 550 | setLocale(locale: string) { 551 | return datetime(this.toDateObj(), { ...this.#option, locale }); 552 | } 553 | 554 | #option(): Option { 555 | return { 556 | offsetMillisec: this.offsetMillisec(), 557 | timezone: this.timezone, 558 | locale: this.locale, 559 | }; 560 | } 561 | } 562 | -------------------------------------------------------------------------------- /datetime_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 2 | import { 3 | DateTime, 4 | datetime, 5 | diffInDays, 6 | diffInHours, 7 | diffInMillisec, 8 | diffInMin, 9 | diffInSec, 10 | latestDateTime, 11 | oldestDateTime, 12 | } from "./datetime.ts"; 13 | import { Timezone } from "./types.ts"; 14 | 15 | Deno.test("parseISO", () => { 16 | const tests = [ 17 | { 18 | input: "2021-01-01", 19 | expected: { 20 | year: 2021, 21 | month: 1, 22 | day: 1, 23 | hour: 0, 24 | minute: 0, 25 | second: 0, 26 | millisecond: 0, 27 | }, 28 | }, 29 | { 30 | input: "2021-01-01T12:30:30.000Z", 31 | expected: { 32 | year: 2021, 33 | month: 1, 34 | day: 1, 35 | hour: 12, 36 | minute: 30, 37 | second: 30, 38 | millisecond: 0, 39 | }, 40 | }, 41 | // normalize offset 42 | { 43 | input: "2021-01-01T12:30:30.000+09:00", 44 | expected: { 45 | year: 2021, 46 | month: 1, 47 | day: 1, 48 | hour: 3, 49 | minute: 30, 50 | second: 30, 51 | millisecond: 0, 52 | }, 53 | }, 54 | ]; 55 | 56 | tests.forEach((t) => { 57 | assertEquals( 58 | datetime(t.input).toDateObj(), 59 | t.expected, 60 | ); 61 | }); 62 | }); 63 | 64 | Deno.test("isValidZone", () => { 65 | const tests = [ 66 | { input: "Asia/Tokyo", expected: true }, 67 | { input: "Fantasia/Castle", expected: false }, 68 | { input: "America/New_York", expected: true }, 69 | { input: "dummy", expected: false }, 70 | { input: "Osaka", expected: false }, 71 | ]; 72 | 73 | tests.forEach((t) => { 74 | assertEquals( 75 | datetime("202101", { timezone: t.input }).isValidZone(), 76 | t.expected, 77 | ); 78 | }); 79 | }); 80 | 81 | Deno.test("isValid", () => { 82 | const tests = [ 83 | { input: { year: 2021, month: 1, day: 1 }, expected: true }, 84 | { input: { year: 2020, month: 2, day: 29 }, expected: true }, 85 | { input: { year: 2021, month: 2, day: 29 }, expected: false }, 86 | { input: { year: 2021, month: 4, day: 31 }, expected: false }, 87 | { input: { year: 2021, month: 11, day: 31 }, expected: false }, 88 | ]; 89 | 90 | tests.forEach((t) => { 91 | assertEquals(datetime(t.input).isValid(), t.expected); 92 | }); 93 | }); 94 | 95 | Deno.test("toDateObj", () => { 96 | const tests = [ 97 | { 98 | stringInput: datetime("2021-01-01T12:30:30.000Z"), 99 | dateObjInput: datetime({ 100 | year: 2021, 101 | month: 1, 102 | day: 1, 103 | hour: 12, 104 | minute: 30, 105 | second: 30, 106 | millisecond: 0, 107 | }), 108 | expected: { 109 | year: 2021, 110 | month: 1, 111 | day: 1, 112 | hour: 12, 113 | minute: 30, 114 | second: 30, 115 | millisecond: 0, 116 | }, 117 | }, 118 | ]; 119 | 120 | tests.forEach((t) => { 121 | assertEquals(t.stringInput.toDateObj(), t.expected); 122 | assertEquals(t.dateObjInput.toDateObj(), t.expected); 123 | }); 124 | }); 125 | 126 | Deno.test("now", () => { 127 | const tests = [ 128 | { 129 | input: datetime(), 130 | jsDate: new Date(), 131 | }, 132 | ]; 133 | 134 | tests.forEach((t) => { 135 | assertEquals( 136 | (t.input.toJSDate().getTime() - t.jsDate.getTime()) < 1000, 137 | true, 138 | ); 139 | }); 140 | }); 141 | 142 | Deno.test("toUTC", () => { 143 | const tests = [ 144 | { 145 | input: datetime("2021-01-01T12:30:30.000Z", { 146 | timezone: "Asia/Tokyo", 147 | }), 148 | expected: { 149 | year: 2021, 150 | month: 1, 151 | day: 1, 152 | hour: 3, 153 | minute: 30, 154 | second: 30, 155 | millisecond: 0, 156 | }, 157 | }, 158 | { 159 | input: datetime("2021-01-01T12:30:30.000Z", { 160 | timezone: "America/New_York", 161 | }), 162 | expected: { 163 | year: 2021, 164 | month: 1, 165 | day: 1, 166 | hour: 17, 167 | minute: 30, 168 | second: 30, 169 | millisecond: 0, 170 | }, 171 | }, 172 | { 173 | input: datetime("2021-05-15T12:30:30.000Z", { 174 | timezone: "America/New_York", 175 | }), 176 | expected: { 177 | year: 2021, 178 | month: 5, 179 | day: 15, 180 | hour: 16, 181 | minute: 30, 182 | second: 30, 183 | millisecond: 0, 184 | }, 185 | }, 186 | { 187 | input: datetime("2021-05-15T12:30:30.000Z", { 188 | timezone: "UTC", 189 | }), 190 | expected: { 191 | year: 2021, 192 | month: 5, 193 | day: 15, 194 | hour: 12, 195 | minute: 30, 196 | second: 30, 197 | millisecond: 0, 198 | }, 199 | }, 200 | ]; 201 | 202 | tests.forEach((t) => { 203 | assertEquals(t.input.toUTC().toDateObj(), t.expected); 204 | }); 205 | }); 206 | 207 | Deno.test("offsetMillisec", () => { 208 | const tests = [ 209 | { 210 | input: datetime("2021-01-01T12:30:30.000Z", { 211 | timezone: "Asia/Tokyo", 212 | }), 213 | expected: 32400000, 214 | }, 215 | { 216 | input: datetime("2021-01-01T12:30:30.000Z", { 217 | timezone: "America/New_York", 218 | }), 219 | expected: -18000000, 220 | }, 221 | { 222 | input: datetime("2021-05-15T12:30:30.000Z", { 223 | timezone: "America/New_York", 224 | }), 225 | expected: -14400000, 226 | }, 227 | { 228 | input: datetime("2021-05-15T12:30:30.000Z", { timezone: "UTC" }), 229 | expected: 0, 230 | }, 231 | ]; 232 | 233 | tests.forEach((t) => { 234 | assertEquals(t.input.offsetMillisec(), t.expected); 235 | }); 236 | }); 237 | 238 | Deno.test("offsetSec", () => { 239 | const tests = [ 240 | { 241 | input: datetime("2021-01-01T12:30:30.000Z", { 242 | timezone: "Asia/Tokyo", 243 | }), 244 | expected: 32400, 245 | }, 246 | { 247 | input: datetime("2021-01-01T12:30:30.000Z", { 248 | timezone: "America/New_York", 249 | }), 250 | expected: -18000, 251 | }, 252 | { 253 | input: datetime("2021-05-15T12:30:30.000Z", { 254 | timezone: "America/New_York", 255 | }), 256 | expected: -14400, 257 | }, 258 | { 259 | input: datetime("2021-05-15T12:30:30.000Z", { timezone: "UTC" }), 260 | expected: 0, 261 | }, 262 | ]; 263 | 264 | tests.forEach((t) => { 265 | assertEquals(t.input.offsetSec(), t.expected); 266 | }); 267 | }); 268 | 269 | Deno.test("offsetMin", () => { 270 | const tests = [ 271 | { 272 | input: datetime("2021-01-01T12:30:30.000Z", { 273 | timezone: "Asia/Tokyo", 274 | }), 275 | expected: 540, 276 | }, 277 | { 278 | input: datetime("2021-01-01T12:30:30.000Z", { 279 | timezone: "America/New_York", 280 | }), 281 | expected: -300, 282 | }, 283 | { 284 | input: datetime("2021-05-15T12:30:30.000Z", { 285 | timezone: "America/New_York", 286 | }), 287 | expected: -240, 288 | }, 289 | { 290 | input: datetime("2021-05-15T12:30:30.000Z", { timezone: "UTC" }), 291 | expected: 0, 292 | }, 293 | ]; 294 | 295 | tests.forEach((t) => { 296 | assertEquals(t.input.offsetMin(), t.expected); 297 | }); 298 | }); 299 | 300 | Deno.test("offsetHour", () => { 301 | const tests = [ 302 | { 303 | input: datetime("2021-01-01T12:30:30.000Z", { 304 | timezone: "Asia/Tokyo", 305 | }), 306 | expected: 9, 307 | }, 308 | { 309 | input: datetime("2021-01-01T12:30:30.000Z", { 310 | timezone: "America/New_York", 311 | }), 312 | expected: -5, 313 | }, 314 | { 315 | input: datetime("2021-05-15T12:30:30.000Z", { 316 | timezone: "America/New_York", 317 | }), 318 | expected: -4, 319 | }, 320 | { 321 | input: datetime("2021-05-15T12:30:30.000Z", { timezone: "UTC" }), 322 | expected: 0, 323 | }, 324 | ]; 325 | 326 | tests.forEach((t) => { 327 | assertEquals(t.input.offsetHour(), t.expected); 328 | }); 329 | }); 330 | 331 | Deno.test("toZonedTime", () => { 332 | type Test = { 333 | input: DateTime; 334 | tz: Timezone; 335 | expected: DateTime; 336 | }; 337 | const tests: Test[] = [ 338 | { 339 | input: datetime("2021-01-01T12:30:30.000Z", { 340 | timezone: "Asia/Tokyo", 341 | }), 342 | tz: "America/New_York", 343 | expected: datetime("2020-12-31T22:30:30.000Z", { 344 | timezone: "America/New_York", 345 | }), 346 | }, 347 | { 348 | input: datetime("2021-01-01T12:30:30.000Z", { 349 | timezone: "UTC", 350 | }), 351 | tz: "Asia/Tokyo", 352 | expected: datetime("2021-01-01T21:30:30.000Z", { 353 | timezone: "Asia/Tokyo", 354 | }), 355 | }, 356 | ]; 357 | 358 | tests.forEach((t) => { 359 | assertEquals(t.input.toZonedTime(t.tz).timezone, t.expected.timezone); 360 | assertEquals( 361 | t.input.toZonedTime(t.tz).toDateObj(), 362 | t.expected.toDateObj(), 363 | ); 364 | }); 365 | }); 366 | 367 | Deno.test("toJSDate", () => { 368 | type Test = { 369 | input: DateTime; 370 | expected: Date; 371 | }; 372 | const tests: Test[] = [ 373 | { 374 | input: datetime("2021-01-01T12:30:30.000Z", { timezone: "UTC" }), 375 | expected: new Date(Date.UTC(2021, 0, 1, 12, 30, 30, 0)), 376 | }, 377 | { 378 | input: datetime("2021-05-15T21:30:30.000Z", { timezone: "UTC" }), 379 | expected: new Date(Date.UTC(2021, 4, 15, 21, 30, 30, 0)), 380 | }, 381 | ]; 382 | 383 | tests.forEach((t) => { 384 | assertEquals(t.input.toJSDate(), t.expected); 385 | }); 386 | }); 387 | 388 | Deno.test("toISODate", () => { 389 | type Test = { 390 | input: DateTime; 391 | expected: string; 392 | }; 393 | const tests: Test[] = [ 394 | { 395 | input: datetime({ 396 | year: 2021, 397 | month: 5, 398 | day: 15, 399 | hour: 12, 400 | minute: 30, 401 | second: 30, 402 | millisecond: 0, 403 | }), 404 | expected: "2021-05-15", 405 | }, 406 | { 407 | input: datetime({ 408 | year: 2021, 409 | month: 7, 410 | day: 21, 411 | hour: 12, 412 | minute: 30, 413 | second: 30, 414 | millisecond: 0, 415 | }), 416 | expected: "2021-07-21", 417 | }, 418 | ]; 419 | 420 | tests.forEach((t) => { 421 | assertEquals(t.input.toISODate(), t.expected); 422 | }); 423 | }); 424 | 425 | Deno.test("toISOWeekDate", () => { 426 | type Test = { 427 | input: DateTime; 428 | expected: string; 429 | }; 430 | const tests: Test[] = [ 431 | { 432 | input: datetime({ 433 | year: 2021, 434 | month: 5, 435 | day: 15, 436 | hour: 12, 437 | minute: 30, 438 | second: 30, 439 | millisecond: 999, 440 | }), 441 | expected: "2021-W19-6", 442 | }, 443 | { 444 | input: datetime({ 445 | year: 2021, 446 | month: 7, 447 | day: 21, 448 | hour: 23, 449 | minute: 0, 450 | second: 59, 451 | millisecond: 0, 452 | }), 453 | expected: "2021-W29-3", 454 | }, 455 | { 456 | input: datetime({ 457 | year: 2021, 458 | month: 1, 459 | day: 1, 460 | hour: 23, 461 | minute: 0, 462 | second: 59, 463 | millisecond: 0, 464 | }), 465 | expected: "2021-W53-5", 466 | }, 467 | { 468 | input: datetime({ 469 | year: 2021, 470 | month: 12, 471 | day: 31, 472 | hour: 23, 473 | minute: 0, 474 | second: 59, 475 | millisecond: 0, 476 | }), 477 | expected: "2021-W52-5", 478 | }, 479 | ]; 480 | 481 | tests.forEach((t) => { 482 | assertEquals(t.input.toISOWeekDate(), t.expected); 483 | }); 484 | }); 485 | 486 | Deno.test("toISOTime", () => { 487 | type Test = { 488 | input: DateTime; 489 | expected: string; 490 | }; 491 | const tests: Test[] = [ 492 | { 493 | input: datetime({ 494 | year: 2021, 495 | month: 5, 496 | day: 15, 497 | hour: 12, 498 | minute: 30, 499 | second: 30, 500 | millisecond: 999, 501 | }), 502 | expected: "12:30:30.999", 503 | }, 504 | { 505 | input: datetime({ 506 | year: 2021, 507 | month: 7, 508 | day: 21, 509 | hour: 23, 510 | minute: 0, 511 | second: 59, 512 | millisecond: 0, 513 | }), 514 | expected: "23:00:59.000", 515 | }, 516 | ]; 517 | 518 | tests.forEach((t) => { 519 | assertEquals(t.input.toISOTime(), t.expected); 520 | }); 521 | }); 522 | 523 | Deno.test("toISO", () => { 524 | type Test = { 525 | input: DateTime; 526 | expected: string; 527 | }; 528 | const tests: Test[] = [ 529 | { 530 | input: datetime({ 531 | year: 2021, 532 | month: 5, 533 | day: 15, 534 | hour: 12, 535 | minute: 30, 536 | second: 30, 537 | millisecond: 999, 538 | }, { timezone: "UTC" }), 539 | expected: "2021-05-15T12:30:30.999Z", 540 | }, 541 | { 542 | input: datetime({ 543 | year: 2021, 544 | month: 7, 545 | day: 21, 546 | hour: 23, 547 | minute: 0, 548 | second: 59, 549 | millisecond: 0, 550 | }, { timezone: "Asia/Tokyo" }), 551 | expected: "2021-07-21T23:00:59.000+09:00", 552 | }, 553 | { 554 | input: datetime({ 555 | year: 2021, 556 | month: 7, 557 | day: 21, 558 | hour: 23, 559 | minute: 0, 560 | second: 59, 561 | millisecond: 0, 562 | }, { timezone: "America/New_York" }), 563 | expected: "2021-07-21T23:00:59.000-04:00", 564 | }, 565 | ]; 566 | 567 | tests.forEach((t) => { 568 | assertEquals(t.input.toISO(), t.expected); 569 | }); 570 | }); 571 | 572 | Deno.test("toArray", () => { 573 | const tests = [ 574 | { input: "2021-07-21", expected: [2021, 7, 21, 0, 0, 0, 0] }, 575 | { input: "2021-07-21T23:00:59", expected: [2021, 7, 21, 23, 0, 59, 0] }, 576 | { 577 | input: "2021-07-21T23:15:59.999", 578 | expected: [2021, 7, 21, 23, 15, 59, 999], 579 | }, 580 | { 581 | input: "2021-07-21T23:15:59.999Z", 582 | expected: [2021, 7, 21, 23, 15, 59, 999], 583 | }, 584 | { 585 | input: "2021-07-21T23:00:59.000Z", 586 | expected: [2021, 7, 21, 23, 0, 59, 0], 587 | }, 588 | ]; 589 | tests.forEach((t) => { 590 | assertEquals(datetime(t.input).toArray(), t.expected); 591 | }); 592 | }); 593 | 594 | Deno.test("toMilliseconds", () => { 595 | const tests = [ 596 | { 597 | input: "2021-07-21T23:00:59", 598 | expected: new Date(2021, 6, 21, 23, 0, 59), 599 | }, 600 | ]; 601 | tests.forEach((t) => { 602 | assertEquals(datetime(t.input).toMilliseconds(), t.expected.getTime()); 603 | }); 604 | }); 605 | 606 | Deno.test("weeksInWeekYear", () => { 607 | const tests = [ 608 | { input: "2021-01-01", expected: 52 }, 609 | { input: "2020-12-31", expected: 53 }, 610 | ]; 611 | tests.forEach((t) => { 612 | assertEquals(datetime(t.input).weeksInWeekYear(), t.expected); 613 | }); 614 | }); 615 | 616 | Deno.test("dayOfYear", () => { 617 | const tests = [ 618 | { input: "2021-01-01", expected: 1 }, 619 | { input: "2021-02-23", expected: 54 }, 620 | { input: "2021-12-31", expected: 365 }, 621 | { input: "2020-12-31", expected: 366 }, 622 | ]; 623 | tests.forEach((t) => { 624 | assertEquals(datetime(t.input).dayOfYear(), t.expected); 625 | }); 626 | }); 627 | 628 | Deno.test("add", () => { 629 | const tests = [ 630 | { 631 | initialDate: "2021-05-31T23:00:00", 632 | addDate: { year: 1 }, 633 | expected: { 634 | year: 2022, 635 | month: 5, 636 | day: 31, 637 | hour: 23, 638 | minute: 0, 639 | second: 0, 640 | millisecond: 0, 641 | }, 642 | }, 643 | { 644 | initialDate: "2021-05-31T23:00:00", 645 | addDate: { month: 1 }, 646 | expected: { 647 | year: 2021, 648 | month: 6, 649 | day: 30, 650 | hour: 23, 651 | minute: 0, 652 | second: 0, 653 | millisecond: 0, 654 | }, 655 | }, 656 | { 657 | initialDate: "2021-02-01T23:00:00", 658 | addDate: { hour: 1 }, 659 | expected: { 660 | year: 2021, 661 | month: 2, 662 | day: 2, 663 | hour: 0, 664 | minute: 0, 665 | second: 0, 666 | millisecond: 0, 667 | }, 668 | }, 669 | { 670 | initialDate: "2021-02-01T23:00:00", 671 | addDate: { minute: 65 }, 672 | expected: { 673 | year: 2021, 674 | month: 2, 675 | day: 2, 676 | hour: 0, 677 | minute: 5, 678 | second: 0, 679 | millisecond: 0, 680 | }, 681 | }, 682 | ]; 683 | tests.forEach((t) => { 684 | assertEquals( 685 | datetime(t.initialDate).add(t.addDate).toDateObj(), 686 | t.expected, 687 | ); 688 | }); 689 | }); 690 | 691 | Deno.test("subtract", () => { 692 | const tests = [ 693 | { 694 | initialDate: "2021-02-01T23:00:00", 695 | subDate: { year: 20 }, 696 | expected: { 697 | year: 2001, 698 | month: 2, 699 | day: 1, 700 | hour: 23, 701 | minute: 0, 702 | second: 0, 703 | millisecond: 0, 704 | }, 705 | }, 706 | { 707 | initialDate: "2021-02-28T23:00:00", 708 | subDate: { month: 2 }, 709 | expected: { 710 | year: 2020, 711 | month: 12, 712 | day: 28, 713 | hour: 23, 714 | minute: 0, 715 | second: 0, 716 | millisecond: 0, 717 | }, 718 | }, 719 | { 720 | initialDate: "2021-02-01T23:00:00", 721 | subDate: { day: 1 }, 722 | expected: { 723 | year: 2021, 724 | month: 1, 725 | day: 31, 726 | hour: 23, 727 | minute: 0, 728 | second: 0, 729 | millisecond: 0, 730 | }, 731 | }, 732 | ]; 733 | tests.forEach((t) => { 734 | assertEquals( 735 | datetime(t.initialDate).subtract(t.subDate) 736 | .toDateObj(), 737 | t.expected, 738 | ); 739 | }); 740 | }); 741 | 742 | Deno.test("diffInDays", () => { 743 | const tests = [ 744 | { 745 | baseDate: "2021-02-01T23:00:00", 746 | otherDate: "2021-02-02T23:00:00", 747 | nonDecimalExpected: 1, 748 | decimalExpected: 1, 749 | }, 750 | { 751 | baseDate: "2021-02-01T23:00:00", 752 | otherDate: "2021-03-01T23:00:00", 753 | nonDecimalExpected: 28, 754 | decimalExpected: 28, 755 | }, 756 | { 757 | baseDate: "2021-02-01T14:00:00", 758 | otherDate: "2021-02-02T23:00:00", 759 | nonDecimalExpected: 1, 760 | decimalExpected: 1.375, 761 | }, 762 | { 763 | baseDate: "2021-01-01T23:00:00", 764 | otherDate: "2021-10-31T22:50:00", 765 | nonDecimalExpected: 302, 766 | decimalExpected: 302.99305555555554, 767 | }, 768 | ]; 769 | tests.forEach((t) => { 770 | assertEquals( 771 | diffInDays( 772 | datetime(t.baseDate), 773 | datetime(t.otherDate), 774 | ), 775 | t.nonDecimalExpected, 776 | ); 777 | assertEquals( 778 | diffInDays( 779 | datetime(t.baseDate), 780 | datetime(t.otherDate), 781 | { showDecimal: true }, 782 | ), 783 | t.decimalExpected, 784 | ); 785 | }); 786 | }); 787 | 788 | Deno.test("diffInHours", () => { 789 | const tests = [ 790 | { 791 | baseDate: "2021-02-01T23:00:00", 792 | otherDate: "2021-02-02T23:00:00", 793 | nonDecimalExpected: 24, 794 | decimalExpected: 24, 795 | }, 796 | { 797 | baseDate: "2021-02-01T00:00:00", 798 | otherDate: "2021-02-03T02:30:00", 799 | nonDecimalExpected: 50, 800 | decimalExpected: 50.5, 801 | }, 802 | { 803 | baseDate: "2021-01-01T00:00:00", 804 | otherDate: "2021-08-29T02:20:00", 805 | nonDecimalExpected: 5762, 806 | decimalExpected: 5762.333333333333, 807 | }, 808 | ]; 809 | tests.forEach((t) => { 810 | assertEquals( 811 | diffInHours( 812 | datetime(t.baseDate), 813 | datetime(t.otherDate), 814 | ), 815 | t.nonDecimalExpected, 816 | ); 817 | assertEquals( 818 | diffInHours( 819 | datetime(t.baseDate), 820 | datetime(t.otherDate), 821 | { showDecimal: true }, 822 | ), 823 | t.decimalExpected, 824 | ); 825 | }); 826 | }); 827 | 828 | Deno.test("diffInMin", () => { 829 | const tests = [ 830 | { 831 | baseDate: "2021-02-01T23:00:00", 832 | otherDate: "2021-02-02T23:00:00", 833 | nonDecimalExpected: 1440, 834 | decimalExpected: 1440, 835 | }, 836 | { 837 | baseDate: "2021-02-01T00:00:00", 838 | otherDate: "2021-02-01T02:30:00", 839 | nonDecimalExpected: 150, 840 | decimalExpected: 150, 841 | }, 842 | { 843 | baseDate: "2021-01-01T00:00:00", 844 | otherDate: "2021-01-01T00:30:59", 845 | nonDecimalExpected: 30, 846 | decimalExpected: 30.983333333333334, 847 | }, 848 | { 849 | baseDate: "2021-01-01T00:00:00", 850 | otherDate: "2021-01-01T02:30:30", 851 | nonDecimalExpected: 150, 852 | decimalExpected: 150.5, 853 | }, 854 | ]; 855 | tests.forEach((t) => { 856 | assertEquals( 857 | diffInMin( 858 | datetime(t.baseDate), 859 | datetime(t.otherDate), 860 | ), 861 | t.nonDecimalExpected, 862 | ); 863 | assertEquals( 864 | diffInMin( 865 | datetime(t.baseDate), 866 | datetime(t.otherDate), 867 | { showDecimal: true }, 868 | ), 869 | t.decimalExpected, 870 | ); 871 | }); 872 | }); 873 | 874 | Deno.test("diffInSec", () => { 875 | const tests = [ 876 | { 877 | baseDate: "2021-02-01T23:00:00", 878 | otherDate: "2021-02-01T23:00:50", 879 | expected: 50, 880 | }, 881 | { 882 | baseDate: "2021-02-01T00:00:00", 883 | otherDate: "2021-02-01T00:30:00", 884 | expected: 1800, 885 | }, 886 | { 887 | baseDate: "2021-01-01T00:00:00", 888 | otherDate: "2021-01-01T00:30:59", 889 | expected: 1859, 890 | }, 891 | ]; 892 | tests.forEach((t) => { 893 | assertEquals( 894 | diffInSec( 895 | datetime(t.baseDate), 896 | datetime(t.otherDate), 897 | ), 898 | t.expected, 899 | ); 900 | }); 901 | }); 902 | 903 | Deno.test("diffInMillisec", () => { 904 | const tests = [ 905 | { 906 | baseDate: "2021-02-01T23:00:00", 907 | otherDate: "2021-02-01T23:00:50.999", 908 | expected: 50999, 909 | }, 910 | { 911 | baseDate: "2021-02-01T00:00:00", 912 | otherDate: "2021-02-01T00:30:00.100", 913 | expected: 1800100, 914 | }, 915 | ]; 916 | tests.forEach((t) => { 917 | assertEquals( 918 | diffInMillisec( 919 | datetime(t.baseDate), 920 | datetime(t.otherDate), 921 | ), 922 | t.expected, 923 | ); 924 | }); 925 | }); 926 | 927 | Deno.test("oldestDateTime", () => { 928 | const tests = [ 929 | { 930 | first: "2021-02-01T10:00:00", 931 | second: "2021-02-01T20:00:50.999", 932 | third: "2021-02-01T23:00:50.999", 933 | }, 934 | ]; 935 | tests.forEach((t) => { 936 | const min = oldestDateTime( 937 | [ 938 | datetime(t.first), 939 | datetime(t.second), 940 | datetime(t.third), 941 | ], 942 | ); 943 | assertEquals( 944 | min.toDateObj(), 945 | datetime(t.first).toDateObj(), 946 | ); 947 | }); 948 | }); 949 | 950 | Deno.test("latestDateTime", () => { 951 | const tests = [ 952 | { 953 | first: "2021-02-01T10:00:00", 954 | second: "2021-02-01T20:00:50.999", 955 | third: "2021-02-01T23:00:50.999", 956 | }, 957 | ]; 958 | tests.forEach((t) => { 959 | const max = latestDateTime( 960 | [ 961 | datetime(t.first), 962 | datetime(t.second), 963 | datetime(t.third), 964 | ], 965 | ); 966 | assertEquals( 967 | max.toDateObj(), 968 | datetime(t.third).toDateObj(), 969 | ); 970 | }); 971 | }); 972 | 973 | Deno.test("startOfYear", () => { 974 | const tests = [ 975 | { 976 | input: "2021-07-28T12:30:30.800Z", 977 | expected: { 978 | year: 2021, 979 | month: 1, 980 | day: 1, 981 | hour: 0, 982 | minute: 0, 983 | second: 0, 984 | millisecond: 0, 985 | }, 986 | }, 987 | ]; 988 | 989 | tests.forEach((t) => { 990 | assertEquals( 991 | datetime(t.input).startOfYear().toDateObj(), 992 | t.expected, 993 | ); 994 | }); 995 | }); 996 | 997 | Deno.test("startOfQuarter", () => { 998 | const tests = [ 999 | { 1000 | input: "2021-03-28T12:30:30.800Z", 1001 | expected: { 1002 | year: 2021, 1003 | month: 1, 1004 | day: 1, 1005 | hour: 0, 1006 | minute: 0, 1007 | second: 0, 1008 | millisecond: 0, 1009 | }, 1010 | }, 1011 | { 1012 | input: "2021-05-28T12:30:30.800Z", 1013 | expected: { 1014 | year: 2021, 1015 | month: 4, 1016 | day: 1, 1017 | hour: 0, 1018 | minute: 0, 1019 | second: 0, 1020 | millisecond: 0, 1021 | }, 1022 | }, 1023 | { 1024 | input: "2021-07-28T12:30:30.800Z", 1025 | expected: { 1026 | year: 2021, 1027 | month: 7, 1028 | day: 1, 1029 | hour: 0, 1030 | minute: 0, 1031 | second: 0, 1032 | millisecond: 0, 1033 | }, 1034 | }, 1035 | { 1036 | input: "2021-11-28T12:30:30.800Z", 1037 | expected: { 1038 | year: 2021, 1039 | month: 10, 1040 | day: 1, 1041 | hour: 0, 1042 | minute: 0, 1043 | second: 0, 1044 | millisecond: 0, 1045 | }, 1046 | }, 1047 | ]; 1048 | 1049 | tests.forEach((t) => { 1050 | assertEquals( 1051 | datetime(t.input).startOfQuarter().toDateObj(), 1052 | t.expected, 1053 | ); 1054 | }); 1055 | }); 1056 | 1057 | Deno.test("startOfMonth", () => { 1058 | const tests = [ 1059 | { 1060 | input: "2021-07-28T12:30:30.800Z", 1061 | expected: { 1062 | year: 2021, 1063 | month: 7, 1064 | day: 1, 1065 | hour: 0, 1066 | minute: 0, 1067 | second: 0, 1068 | millisecond: 0, 1069 | }, 1070 | }, 1071 | ]; 1072 | 1073 | tests.forEach((t) => { 1074 | assertEquals( 1075 | datetime(t.input).startOfMonth().toDateObj(), 1076 | t.expected, 1077 | ); 1078 | }); 1079 | }); 1080 | 1081 | Deno.test("startOfDay", () => { 1082 | const tests = [ 1083 | { 1084 | input: "2021-07-28T12:30:30.800Z", 1085 | expected: { 1086 | year: 2021, 1087 | month: 7, 1088 | day: 28, 1089 | hour: 0, 1090 | minute: 0, 1091 | second: 0, 1092 | millisecond: 0, 1093 | }, 1094 | }, 1095 | ]; 1096 | 1097 | tests.forEach((t) => { 1098 | assertEquals( 1099 | datetime(t.input).startOfDay().toDateObj(), 1100 | t.expected, 1101 | ); 1102 | }); 1103 | }); 1104 | 1105 | Deno.test("startOfHour", () => { 1106 | const tests = [ 1107 | { 1108 | input: "2021-07-28T12:30:30.800Z", 1109 | expected: { 1110 | year: 2021, 1111 | month: 7, 1112 | day: 28, 1113 | hour: 12, 1114 | minute: 0, 1115 | second: 0, 1116 | millisecond: 0, 1117 | }, 1118 | }, 1119 | ]; 1120 | 1121 | tests.forEach((t) => { 1122 | assertEquals( 1123 | datetime(t.input).startOfHour().toDateObj(), 1124 | t.expected, 1125 | ); 1126 | }); 1127 | }); 1128 | 1129 | Deno.test("startOfMinute", () => { 1130 | const tests = [ 1131 | { 1132 | input: "2021-07-28T12:30:30.800Z", 1133 | expected: { 1134 | year: 2021, 1135 | month: 7, 1136 | day: 28, 1137 | hour: 12, 1138 | minute: 30, 1139 | second: 0, 1140 | millisecond: 0, 1141 | }, 1142 | }, 1143 | ]; 1144 | 1145 | tests.forEach((t) => { 1146 | assertEquals( 1147 | datetime(t.input).startOfMinute().toDateObj(), 1148 | t.expected, 1149 | ); 1150 | }); 1151 | }); 1152 | 1153 | Deno.test("startOfSecond", () => { 1154 | const tests = [ 1155 | { 1156 | input: "2021-07-28T12:30:30.800Z", 1157 | expected: { 1158 | year: 2021, 1159 | month: 7, 1160 | day: 28, 1161 | hour: 12, 1162 | minute: 30, 1163 | second: 30, 1164 | millisecond: 0, 1165 | }, 1166 | }, 1167 | ]; 1168 | 1169 | tests.forEach((t) => { 1170 | assertEquals( 1171 | datetime(t.input).startOfSecond().toDateObj(), 1172 | t.expected, 1173 | ); 1174 | }); 1175 | }); 1176 | 1177 | Deno.test("endOfYear", () => { 1178 | const tests = [ 1179 | { 1180 | input: "2021-07-28T12:30:30.800Z", 1181 | expected: { 1182 | year: 2021, 1183 | month: 12, 1184 | day: 31, 1185 | hour: 23, 1186 | minute: 59, 1187 | second: 59, 1188 | millisecond: 999, 1189 | }, 1190 | }, 1191 | ]; 1192 | 1193 | tests.forEach((t) => { 1194 | assertEquals( 1195 | datetime(t.input).endOfYear().toDateObj(), 1196 | t.expected, 1197 | ); 1198 | }); 1199 | }); 1200 | 1201 | Deno.test("endOfQuarter", () => { 1202 | const tests = [ 1203 | { 1204 | input: "2021-03-28T12:30:30.800Z", 1205 | expected: { 1206 | year: 2021, 1207 | month: 3, 1208 | day: 31, 1209 | hour: 23, 1210 | minute: 59, 1211 | second: 59, 1212 | millisecond: 999, 1213 | }, 1214 | }, 1215 | { 1216 | input: "2021-05-28T12:30:30.800Z", 1217 | expected: { 1218 | year: 2021, 1219 | month: 6, 1220 | day: 30, 1221 | hour: 23, 1222 | minute: 59, 1223 | second: 59, 1224 | millisecond: 999, 1225 | }, 1226 | }, 1227 | { 1228 | input: "2021-07-28T12:30:30.800Z", 1229 | expected: { 1230 | year: 2021, 1231 | month: 9, 1232 | day: 30, 1233 | hour: 23, 1234 | minute: 59, 1235 | second: 59, 1236 | millisecond: 999, 1237 | }, 1238 | }, 1239 | { 1240 | input: "2021-11-28T12:30:30.800Z", 1241 | expected: { 1242 | year: 2021, 1243 | month: 12, 1244 | day: 31, 1245 | hour: 23, 1246 | minute: 59, 1247 | second: 59, 1248 | millisecond: 999, 1249 | }, 1250 | }, 1251 | ]; 1252 | 1253 | tests.forEach((t) => { 1254 | assertEquals( 1255 | datetime(t.input).endOfQuarter().toDateObj(), 1256 | t.expected, 1257 | ); 1258 | }); 1259 | }); 1260 | 1261 | Deno.test("endOfMonth", () => { 1262 | const tests = [ 1263 | { 1264 | input: "2021-07-28T12:30:30.800Z", 1265 | expected: { 1266 | year: 2021, 1267 | month: 7, 1268 | day: 31, 1269 | hour: 23, 1270 | minute: 59, 1271 | second: 59, 1272 | millisecond: 999, 1273 | }, 1274 | }, 1275 | ]; 1276 | 1277 | tests.forEach((t) => { 1278 | assertEquals( 1279 | datetime(t.input).endOfMonth().toDateObj(), 1280 | t.expected, 1281 | ); 1282 | }); 1283 | }); 1284 | 1285 | Deno.test("endOfDay", () => { 1286 | const tests = [ 1287 | { 1288 | input: "2021-07-28T12:30:30.800Z", 1289 | expected: { 1290 | year: 2021, 1291 | month: 7, 1292 | day: 28, 1293 | hour: 23, 1294 | minute: 59, 1295 | second: 59, 1296 | millisecond: 999, 1297 | }, 1298 | }, 1299 | ]; 1300 | 1301 | tests.forEach((t) => { 1302 | assertEquals( 1303 | datetime(t.input).endOfDay().toDateObj(), 1304 | t.expected, 1305 | ); 1306 | }); 1307 | }); 1308 | 1309 | Deno.test("endOfHour", () => { 1310 | const tests = [ 1311 | { 1312 | input: "2021-07-28T12:30:30.800Z", 1313 | expected: { 1314 | year: 2021, 1315 | month: 7, 1316 | day: 28, 1317 | hour: 12, 1318 | minute: 59, 1319 | second: 59, 1320 | millisecond: 999, 1321 | }, 1322 | }, 1323 | ]; 1324 | 1325 | tests.forEach((t) => { 1326 | assertEquals( 1327 | datetime(t.input).endOfHour().toDateObj(), 1328 | t.expected, 1329 | ); 1330 | }); 1331 | }); 1332 | 1333 | Deno.test("endOfMinute", () => { 1334 | const tests = [ 1335 | { 1336 | input: "2021-07-28T12:30:30.800Z", 1337 | expected: { 1338 | year: 2021, 1339 | month: 7, 1340 | day: 28, 1341 | hour: 12, 1342 | minute: 30, 1343 | second: 59, 1344 | millisecond: 999, 1345 | }, 1346 | }, 1347 | ]; 1348 | 1349 | tests.forEach((t) => { 1350 | assertEquals( 1351 | datetime(t.input).endOfMinute().toDateObj(), 1352 | t.expected, 1353 | ); 1354 | }); 1355 | }); 1356 | 1357 | Deno.test("endOfSecond", () => { 1358 | const tests = [ 1359 | { 1360 | input: "2021-07-28T12:30:30.800Z", 1361 | expected: { 1362 | year: 2021, 1363 | month: 7, 1364 | day: 28, 1365 | hour: 12, 1366 | minute: 30, 1367 | second: 30, 1368 | millisecond: 999, 1369 | }, 1370 | }, 1371 | ]; 1372 | 1373 | tests.forEach((t) => { 1374 | assertEquals( 1375 | datetime(t.input).endOfSecond().toDateObj(), 1376 | t.expected, 1377 | ); 1378 | }); 1379 | }); 1380 | 1381 | Deno.test("isBefore", () => { 1382 | const tests = [ 1383 | { 1384 | input: "2021-07-28T12:30:30.800Z", 1385 | other: "2021-03-28T12:30:30.999Z", 1386 | expected: false, 1387 | }, 1388 | { 1389 | input: "2021-07-28T12:30:30.800Z", 1390 | other: "2021-12-01T12:30:30.999Z", 1391 | expected: true, 1392 | }, 1393 | ]; 1394 | 1395 | tests.forEach((t) => { 1396 | assertEquals( 1397 | datetime(t.input).isBefore(datetime(t.other)), 1398 | t.expected, 1399 | ); 1400 | }); 1401 | }); 1402 | 1403 | Deno.test("isAfter", () => { 1404 | const tests = [ 1405 | { 1406 | input: "2021-07-28T12:30:30.800Z", 1407 | other: "2021-03-28T12:30:30.999Z", 1408 | expected: true, 1409 | }, 1410 | { 1411 | input: "2021-07-28T12:30:30.800Z", 1412 | other: "2021-12-01T12:30:30.999Z", 1413 | expected: false, 1414 | }, 1415 | ]; 1416 | 1417 | tests.forEach((t) => { 1418 | assertEquals( 1419 | datetime(t.input).isAfter(datetime(t.other)), 1420 | t.expected, 1421 | ); 1422 | }); 1423 | }); 1424 | 1425 | Deno.test("isBetween", () => { 1426 | const tests = [ 1427 | { 1428 | input: "2021-07-28T12:30:30.800Z", 1429 | startDate: "2021-03-28T12:30:30.999Z", 1430 | endDate: "2021-11-28T21:30:30.999Z", 1431 | expected: true, 1432 | }, 1433 | { 1434 | input: "2021-07-28T12:30:30.800Z", 1435 | startDate: "2021-03-28T12:30:30.999Z", 1436 | endDate: "2021-05-28T21:30:30.999Z", 1437 | expected: false, 1438 | }, 1439 | { 1440 | input: "2021-07-28T12:30:30.800Z", 1441 | startDate: "2021-07-28T12:30:30.800", 1442 | endDate: "2021-11-28T21:30:30.999Z", 1443 | expected: true, 1444 | }, 1445 | { 1446 | input: "2021-07-28T12:30:30.800Z", 1447 | startDate: "2021-04-28T12:30:30.800", 1448 | endDate: "2021-07-28T12:30:30.800", 1449 | expected: true, 1450 | }, 1451 | ]; 1452 | 1453 | tests.forEach((t) => { 1454 | assertEquals( 1455 | datetime(t.input).isBetween(datetime(t.startDate), datetime(t.endDate)), 1456 | t.expected, 1457 | ); 1458 | }); 1459 | }); 1460 | 1461 | Deno.test("weekDay", () => { 1462 | const tests = [ 1463 | { 1464 | input: { 1465 | year: 2021, 1466 | month: 1, 1467 | day: 2, 1468 | hour: 0, 1469 | minute: 0, 1470 | second: 0, 1471 | millisecond: 0, 1472 | }, 1473 | expected: 6, 1474 | }, 1475 | { 1476 | input: { 1477 | year: 2021, 1478 | month: 1, 1479 | day: 3, 1480 | hour: 0, 1481 | minute: 0, 1482 | second: 0, 1483 | millisecond: 0, 1484 | }, 1485 | expected: 0, 1486 | }, 1487 | { 1488 | input: { 1489 | year: 2021, 1490 | month: 5, 1491 | day: 3, 1492 | hour: 0, 1493 | minute: 0, 1494 | second: 0, 1495 | millisecond: 0, 1496 | }, 1497 | expected: 1, 1498 | }, 1499 | { 1500 | input: { 1501 | year: 2021, 1502 | month: 5, 1503 | day: 7, 1504 | hour: 0, 1505 | minute: 0, 1506 | second: 0, 1507 | millisecond: 0, 1508 | }, 1509 | expected: 5, 1510 | }, 1511 | ]; 1512 | tests.forEach((t) => { 1513 | assertEquals(datetime(t.input).weekDay(), t.expected); 1514 | }); 1515 | }); 1516 | 1517 | Deno.test("weekDayShort", () => { 1518 | const tests = [ 1519 | { 1520 | input: { 1521 | year: 2021, 1522 | month: 1, 1523 | day: 2, 1524 | hour: 0, 1525 | minute: 0, 1526 | second: 0, 1527 | millisecond: 0, 1528 | }, 1529 | expected: "Sat", 1530 | }, 1531 | { 1532 | input: { 1533 | year: 2021, 1534 | month: 1, 1535 | day: 3, 1536 | hour: 0, 1537 | minute: 0, 1538 | second: 0, 1539 | millisecond: 0, 1540 | }, 1541 | expected: "Sun", 1542 | }, 1543 | { 1544 | input: { 1545 | year: 2021, 1546 | month: 5, 1547 | day: 3, 1548 | hour: 0, 1549 | minute: 0, 1550 | second: 0, 1551 | millisecond: 0, 1552 | }, 1553 | expected: "Mon", 1554 | }, 1555 | { 1556 | input: { 1557 | year: 2021, 1558 | month: 5, 1559 | day: 7, 1560 | hour: 0, 1561 | minute: 0, 1562 | second: 0, 1563 | millisecond: 0, 1564 | }, 1565 | expected: "Fri", 1566 | }, 1567 | ]; 1568 | tests.forEach((t) => { 1569 | assertEquals(datetime(t.input).weekDayShort(), t.expected); 1570 | }); 1571 | }); 1572 | 1573 | Deno.test("weekDayLong", () => { 1574 | const tests = [ 1575 | { 1576 | input: { 1577 | year: 2021, 1578 | month: 1, 1579 | day: 2, 1580 | hour: 0, 1581 | minute: 0, 1582 | second: 0, 1583 | millisecond: 0, 1584 | }, 1585 | expected: "Saturday", 1586 | }, 1587 | { 1588 | input: { 1589 | year: 2021, 1590 | month: 1, 1591 | day: 3, 1592 | hour: 0, 1593 | minute: 0, 1594 | second: 0, 1595 | millisecond: 0, 1596 | }, 1597 | expected: "Sunday", 1598 | }, 1599 | { 1600 | input: { 1601 | year: 2021, 1602 | month: 5, 1603 | day: 3, 1604 | hour: 0, 1605 | minute: 0, 1606 | second: 0, 1607 | millisecond: 0, 1608 | }, 1609 | expected: "Monday", 1610 | }, 1611 | { 1612 | input: { 1613 | year: 2021, 1614 | month: 5, 1615 | day: 7, 1616 | hour: 0, 1617 | minute: 0, 1618 | second: 0, 1619 | millisecond: 0, 1620 | }, 1621 | expected: "Friday", 1622 | }, 1623 | ]; 1624 | tests.forEach((t) => { 1625 | assertEquals(datetime(t.input).weekDayLong(), t.expected); 1626 | }); 1627 | }); 1628 | 1629 | Deno.test("monthShort", () => { 1630 | const tests = [ 1631 | { 1632 | input: "2021-01-28T12:30:30.800Z", 1633 | expected: "Jan", 1634 | }, 1635 | { 1636 | input: "2021-07-28T12:30:30.800Z", 1637 | expected: "Jul", 1638 | }, 1639 | { 1640 | input: "2021-12-28T12:30:30.800Z", 1641 | expected: "Dec", 1642 | }, 1643 | ]; 1644 | tests.forEach((t) => { 1645 | assertEquals(datetime(t.input).monthShort(), t.expected); 1646 | }); 1647 | }); 1648 | 1649 | Deno.test("monthLong", () => { 1650 | const tests = [ 1651 | { 1652 | input: "2021-01-28T12:30:30.800Z", 1653 | expected: "January", 1654 | }, 1655 | { 1656 | input: "2021-07-28T12:30:30.800Z", 1657 | expected: "July", 1658 | }, 1659 | { 1660 | input: "2021-12-28T12:30:30.800Z", 1661 | expected: "December", 1662 | }, 1663 | ]; 1664 | tests.forEach((t) => { 1665 | assertEquals(datetime(t.input).monthLong(), t.expected); 1666 | }); 1667 | }); 1668 | -------------------------------------------------------------------------------- /diff.ts: -------------------------------------------------------------------------------- 1 | import { DateDiff, DateObj } from "./types.ts"; 2 | import { daysInMonth, truncNumber } from "./utils.ts"; 3 | import { dateToTS } from "./convert.ts"; 4 | 5 | export function adjustedTS( 6 | baseDateObj: DateObj, 7 | diff: DateDiff, 8 | option: { 9 | positive: boolean; 10 | }, 11 | ) { 12 | const { 13 | year: baseYear, 14 | month: baseMonth, 15 | day: baseDay, 16 | hour: basehour, 17 | minute: baseminute, 18 | second: basesecond, 19 | millisecond: baseMillisecond, 20 | } = baseDateObj; 21 | 22 | const sign = option.positive ? 1 : -1; 23 | 24 | const diffYear = diff.year && diff.quarter 25 | ? truncNumber(diff.year + diff.quarter * 3) 26 | : truncNumber(diff.year); 27 | const adjustedYear = baseYear + (sign * diffYear); 28 | 29 | const diffMonth = truncNumber(diff.month); 30 | const adjustedMonth = baseMonth + (sign * diffMonth); 31 | 32 | const diffDay = diff.day && diff.weeks 33 | ? truncNumber(diff.day + diff.weeks * 7) 34 | : truncNumber(diff.day); 35 | const diffhour = truncNumber(diff.hour); 36 | const diffminute = truncNumber(diff.minute); 37 | const diffsecond = truncNumber(diff.second); 38 | const diffMillisecond = truncNumber(diff.millisecond); 39 | 40 | return dateToTS({ 41 | year: adjustedYear, 42 | month: adjustedMonth, 43 | day: baseDay 44 | ? Math.min(baseDay, daysInMonth(adjustedYear, adjustedMonth)) + 45 | (sign * diffDay) 46 | : (sign * diffDay), 47 | hour: basehour ? basehour + (sign * diffhour) : (sign * diffhour), 48 | minute: baseminute ? baseminute + (sign * diffminute) : (sign * diffminute), 49 | second: basesecond ? basesecond + (sign * diffsecond) : (sign * diffsecond), 50 | millisecond: baseMillisecond 51 | ? baseMillisecond + (sign * diffMillisecond) 52 | : (sign * diffMillisecond), 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | title: Ptera 3 | description: DateTime library for Deno 4 | -------------------------------------------------------------------------------- /docs/format.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Format 4 | nav_order: 3 5 | --- 6 | 7 | # Format 8 | 9 | ## ISO 8601 10 | 11 | ```typescript 12 | const dt = datetime({ 13 | year: 2021, 14 | month: 7, 15 | day: 21, 16 | hour: 23, 17 | minute: 30, 18 | second: 59, 19 | }); 20 | dt.toISO(); // 2021-07-21T23:30:59.000Z 21 | dt.toISODate(); // 2021-07-21 22 | dt.toISOWeekDate(); // 2021-W29-3 23 | dt.toISOTime(); // 23:30:59.000 24 | ``` 25 | 26 | ## Intl 27 | 28 | Ptera supports native Intl.DateTimeFormat 29 | 30 | ```typescript 31 | const dt = datetime("2021-07-03").setLocale("fr"); 32 | dt.toDateTimeFormat({ dateStyle: "full" }); // samedi 3 juillet 2021; 33 | ``` 34 | 35 | ## Custom Format 36 | 37 | Ptera supports custom format. 38 | 39 | ```typescript 40 | datetime("2021-07-03").format("YYYY/MMMM/dd"); // 2021/July/03 41 | datetime("2021-07-03").setLocale("fr").format("YYYY/MMMM/dd"); // 2021/juillet/03 42 | ``` 43 | 44 | You can escape string by using single quotes. 45 | 46 | ```typescript 47 | datetime("2021-07-03").format("'Year is: 'YYYY"); // Year is: 2021 48 | ``` 49 | 50 | ### Available formats 51 | 52 | | Format | Description | Example | 53 | | ------ | :---------------------------------------- | :-------------------- | 54 | | YY | year, two digits | 21 | 55 | | YYYY | year, four digits | 2021 | 56 | | M | month, one or Two digits | 6 | 57 | | MM | month, two digits | 06 | 58 | | MMM | short month string | Aug | 59 | | MMMM | long month string | August | 60 | | d | day, one or two digits | 8 | 61 | | dd | day, two digits | 08 | 62 | | D | day of year, between one and three digits | 29 | 63 | | DDD | day of year, between one and three digits | 365 | 64 | | H | 24hour, one or two digits | 9 | 65 | | HH | 24hour, two digits | 13 | 66 | | h | 12hour, one or two digits | 2 | 67 | | hh | 12hour, two digits | 11 | 68 | | m | minutes, one or two digits | 45 | 69 | | mm | minutes, two digits | 45 | 70 | | s | seconds, one or two digits | 30 | 71 | | ss | seconds, two digits | 07 | 72 | | S | milliseconds, three digits | 999 | 73 | | w | day of the week, 1 is Monday, 7 is Sunday | 7 | 74 | | www | short week string | Fri | 75 | | wwww | long week string | Friday | 76 | | W | iso week number, one or two digits | 52 | 77 | | WW | iso week number, two digits | 52 | 78 | | a | AM or PM | AM | 79 | | X | Unix timestamp seconds | 1609507800 | 80 | | x | Unix timestamp milliseconds | 1609507800000 | 81 | | z | Timezone | Asia/Tokyo | 82 | | Z | offset with colon | +03:00 | 83 | | ZZ | short offset | +0300 | 84 | | ZZZ | short offset name | UTC-5 | 85 | | ZZZZ | long offset name | Eastern Standard Time | 86 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | nav_order: 1 5 | descriptions: Ptera is DateTime library for Deno 6 | --- 7 | 8 | # Ptera 9 | 10 |

ptera-log

11 | 12 | Ptera is DateTime library for Deno 13 | 14 | [Get Started](https://tak-iwamoto.github.io/ptera/quick_tour.html){: .btn 15 | .btn-blue } 16 | 17 | ## Features 18 | 19 | - Immutable and Chainable 20 | - Parsing and Formatting 21 | - Timezone and Intl support 22 | - Fully Written In Deno 23 | -------------------------------------------------------------------------------- /docs/math.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Math 4 | nav_order: 5 5 | --- 6 | 7 | # Math 8 | 9 | ## add, subtract 10 | 11 | You can manipulate datetime by using `add` or `subtract`. 12 | 13 | ### Support properties 14 | 15 | - year 16 | - month 17 | - day 18 | - hour 19 | - minute 20 | - second 21 | - millisecond 22 | - quarter 23 | - weeks 24 | 25 | ```typescript 26 | const dt = datetime("2021-08-21:13:30:00"); // { year: 2021, month: 8, day: 21, hour: 13, minute: 30, second: 0, millisecond: 0, } 27 | 28 | dt.add({ year: 1, second: 10 }).subtract({ month: 2, minute: 5 }); // { year: 2022, month: 6, day: 21, hour: 13, minute: 25, second: 10, millisecond: 0, } 29 | ``` 30 | 31 | ## start and end 32 | 33 | These methods return the start and end of the unit time. 34 | 35 | ### Available Methods 36 | 37 | - startOfYear 38 | - startOfQuarter 39 | - startOfMonth 40 | - startOfDay 41 | - startOfHour 42 | - startOfMinute 43 | - startOfSecond 44 | - endOfYear 45 | - endOfQuarter 46 | - endOfMonth 47 | - endOfDay 48 | - endOfHour 49 | - endOfMinute 50 | - endOfSecond 51 | 52 | ```typescript 53 | const dt = datetime("2021-08-21:13:30:00").startOfYear(); 54 | // { year: 2021, month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0, } 55 | ``` 56 | 57 | ## diff 58 | 59 | Ptera supports these functions to calculate difference between two datetime. 60 | 61 | - `diffInMillisec` 62 | - `diffInSec` 63 | - `diffInMin` 64 | - `diffInHours` 65 | - `diffInDays` 66 | 67 | `diffInMin`, `diffInHours`, `diffInDays` support `showDecimal` option. 68 | 69 | ```typescript 70 | import { diffInDays, diffInMillisec } from "https://deno.land/x/ptera/mod.ts"; 71 | const dt1 = datetime("2021-08-21:13:30:00"); 72 | const dt2 = datetime("2021-01-30:21:30:00"); 73 | 74 | diffInMillisec(dt1, dt2); // 17510400000 75 | diffInDays(dt1, dt2); // 202 76 | diffInDays(dt1, dt2, { showDecimal: true }); // 202.66666666666666 77 | ``` 78 | 79 | ## latest, oldest 80 | 81 | Extracts the latest or oldest date 82 | 83 | ```typescript 84 | import { 85 | latestDateTime, 86 | oldestDateTime, 87 | } from "https://deno.land/x/ptera/mod.ts"; 88 | 89 | const dt1 = datetime("2021-08-21"); 90 | const dt2 = datetime("2021-01-30"); 91 | const dt3 = datetime("2021-04-30"); 92 | const datetimes = [dt1, dt2, dt3]; 93 | 94 | latestDateTime(datetimes); // dt1 95 | oldestDateTime(datetimes); // dt2 96 | ``` 97 | -------------------------------------------------------------------------------- /docs/parse.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Parse 4 | nav_order: 2 5 | --- 6 | 7 | # Parse 8 | 9 | ## String 10 | 11 | parse ISO 8601. 12 | 13 | ```typescript 14 | // parse ISO 8601 15 | datetime("2021-06-30T21:15:30.200"); 16 | ``` 17 | 18 | You can also parse custom format by using `parse`. 19 | 20 | Ptera also supports parsing intl string. 21 | 22 | ```typescript 23 | const dt = datetime().parse( 24 | "5/Aug/2021:14:15:30 +0900", 25 | "d/MMM/YYYY:HH:mm:ss ZZ", 26 | ); 27 | 28 | // support locale 29 | datetime().parse("2021 лютий 03", "YYYY MMMM dd", { locale: "uk" }); 30 | ``` 31 | 32 | ### Available Formats 33 | 34 | | Format | Description | Example | 35 | | ------ | :---------------------------------------- | :------ | 36 | | YYYY | year, four digits | 2021 | 37 | | M | month, one or Two digits | 6 | 38 | | MM | month, two digits | 06 | 39 | | MMM | short month string | Aug | 40 | | MMMM | long month string | August | 41 | | d | day, one or two digits | 8 | 42 | | dd | day, two digits | 08 | 43 | | D | day of year, between one and three digits | 29 | 44 | | DDD | day of year, between one and three digits | 365 | 45 | | H | 24hour, one or two digits | 9 | 46 | | HH | 24hour, two digits | 13 | 47 | | h | 12hour, one or two digits | 02 | 48 | | hh | 12hour, two digits | 11 | 49 | | m | minutes, one or two digits | 45 | 50 | | mm | minutes, two digits | 45 | 51 | | s | seconds, one or two digits | 30 | 52 | | ss | seconds, two digits | 07 | 53 | | S | milliseconds, three digits | 999 | 54 | | w | day of the week, 1 is Monday, 7 is Sunday | 7 | 55 | | www | short week string | Fri | 56 | | wwww | long week string | Friday | 57 | | a | AM or PM | AM | 58 | | Z | offset with colon | +03:00 | 59 | | ZZ | short offset | +0300 | 60 | 61 | ## Unix Timestamp 62 | 63 | Ptera supports milliseconds Unix Timestamp. 64 | 65 | If you want to parse seconds Unix Timestamp, please convert to milliseconds 66 | (timestamp * 1000). 67 | 68 | ```typescript 69 | datetime(1625238137000); 70 | ``` 71 | 72 | ## Date 73 | 74 | ```typescript 75 | datetime(new Date(2021, 3, 10)); 76 | ``` 77 | 78 | ## Array 79 | 80 | ```typescript 81 | datetime([2021, 6, 25]); // 2021-06-25 82 | datetime([2021, 6, 25, 13, 40, 30, 10]); // 2021-06-25T13:40:30.010 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/quick_tour.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Quick Tour 4 | nav_order: 1 5 | --- 6 | 7 | # Quick Tour 8 | 9 | ## datetime 10 | 11 | The main function is `datetime`. 12 | 13 | You can get the current time by calling the `datetime` function with no 14 | arguments. 15 | 16 | The default timezone is local time. 17 | 18 | ```typescript 19 | import { datetime } from "https://deno.land/x/ptera/mod.ts"; 20 | 21 | // now in localtime 22 | const dt = datetime(); 23 | // DateTime { 24 | // year: 2021, 25 | // month: 7, 26 | // day: 8, 27 | // hour: 23, 28 | // minute: 23, 29 | // second: 57, 30 | // millisecond: 580, 31 | // timezone: "Asia/Tokyo", 32 | // valid: true, 33 | // locale: "en" 34 | // } 35 | 36 | // utc 37 | dt.toUTC(); 38 | // DateTime { 39 | // year: 2021, 40 | // month: 7, 41 | // day: 8, 42 | // hour: 14, 43 | // minute: 23, 44 | // second: 57, 45 | // millisecond: 580, 46 | // timezone: "UTC", 47 | // valid: true, 48 | // locale: "en" 49 | // } 50 | ``` 51 | 52 | `datetime` takes several types of arguments, ISO 8601 string, Date, Object, 53 | milliseconds unixtime, array. 54 | 55 | ```typescript 56 | // parse ISO 8601 57 | datetime("2021-06-30T21:15:30.200"); 58 | 59 | // JavaScript Date 60 | datetime(new Date()); 61 | 62 | // Object 63 | datetime({ year: 2021, month: 3, day: 21 }); 64 | 65 | // Unixtime 66 | datetime(1625238137000); 67 | 68 | // Array 69 | datetime([2021, 6, 11, 13, 30, 30]); 70 | ``` 71 | 72 | ### Properties 73 | 74 | ```typescript 75 | const dt = datetime("2021-06-30T21:15:30.200"); 76 | dt.year; // 2021 77 | dt.month; // 6 78 | dt.day; // 30 79 | dt.hour; // 21 80 | dt.minute; // 15 81 | dt.second; // 30 82 | dt.millisecond; // 200 83 | dt.timezone; 84 | dt.locale; 85 | ``` 86 | 87 | ### Utilities 88 | 89 | ```typescript 90 | const dt = datetime(); 91 | dt.isLeapYear(); 92 | dt.isBefore(); 93 | dt.dayOfYear(); 94 | ``` 95 | 96 | ### Format 97 | 98 | ```typescript 99 | dt.format("YYYY-MM-dd Z"); // 2021-08-21 +03:00 100 | dt.format("MMMM ZZZ"); // January JST 101 | ``` 102 | 103 | ### Timezone 104 | 105 | ```typescript 106 | const dt = datetime("2021-07-21"); 107 | dt.toZonedTime("America/New_York"); // change to other zoned time 108 | ``` 109 | 110 | ### Intl 111 | 112 | ```typescript 113 | const dt = datetime().setLocale("ja"); 114 | dt.format("MMMM www"); // 7月 金 115 | dt.setLocale("fr").format("MMMM www"); // juillet ven. 116 | ``` 117 | 118 | ### Diff 119 | 120 | ```typescript 121 | import { diffInMillisec } from "https://deno.land/x/ptera/mod.ts"; 122 | diffInMillisec(datetime("2021-08-29"), datetime("2021-12-21")); 123 | ``` 124 | -------------------------------------------------------------------------------- /docs/timezone.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Timezone 4 | nav_order: 4 5 | --- 6 | 7 | # Timezone 8 | 9 | Timezone can be set by using `toZonedTime` 10 | 11 | ```typescript 12 | datetime().toZonedTime("America/New_York"); 13 | ``` 14 | 15 | ## Offset 16 | 17 | ```typescript 18 | const dt = datetime().toZonedTime("America/New_York"); 19 | dt.offsetHour(); // -4 20 | dt.offsetMin(); // -240 21 | dt.offsetSec(); // -14400 22 | dt.offsetMillisec(); // -14400000 23 | ``` 24 | 25 | ## UTC 26 | 27 | `toUTC` converts to UTC datetime 28 | 29 | ```typescript 30 | // { year: 2021, month: 7, day: 21, hour: 21, minute: 30, second: 0, millisecond: 0, } 31 | const dt = datetime("2021-07-21T21:30:00", { timezone: "America/New_York" }); 32 | // { year: 2021, month: 7, day: 22, hour: 1, minute: 30, second: 0, millisecond: 0,} 33 | const utc = dt.toUTC(); 34 | ``` 35 | 36 | ## Convert to other zoned time 37 | 38 | ```typescript 39 | // { year: 2021, month: 7, day: 21, hour: 21, minute: 30, second: 0, millisecond: 0, } 40 | const NewYork = datetime("2021-07-21T21:30:00", { 41 | timezone: "America/New_York", 42 | }); 43 | // { year: 2021, month: 7, day: 22, hour: 10, minute: 30, second: 0, millisecond: 0, } 44 | const Tokyo = dt.toZonedTime("Asia/Tokyo"); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/utils.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Utils 4 | nav_order: 6 5 | --- 6 | 7 | # Utils 8 | 9 | ## isBefore 10 | 11 | Check if the datetime is in the past If no argument is given, compare with the 12 | current time. 13 | 14 | ```typescript 15 | datetime("1992-01-01").isBefore(); // true 16 | datetime("2045-01-01").isBefore(); // false 17 | datetime("2045-01-01").isBefore(datetime("2046-01-01")); // true 18 | ``` 19 | 20 | ## isAfter 21 | 22 | Check if the datetime is in the future 23 | 24 | ```typescript 25 | datetime("1992-01-01").isAfter(); // false 26 | datetime("2045-01-01").isAfter(); // true 27 | datetime("2045-01-01").isAfter(datetime("2030-01-01")); // true 28 | ``` 29 | 30 | ## isLeapYear 31 | 32 | Check if the datetime is in leap year 33 | 34 | ```typescript 35 | datetime("2020-01-01").isLeapYear(); // true 36 | datetime("2021-01-01").isLeapYear(); // false 37 | ``` 38 | 39 | ## isValid 40 | 41 | Check if the datetime is valid 42 | 43 | ```typescript 44 | datetime("2020-01-01").isValid(); // true 45 | datetime("2021-13-01").isValid(); // false 46 | ``` 47 | -------------------------------------------------------------------------------- /format.ts: -------------------------------------------------------------------------------- 1 | import { DateFormatType, DateObj, isFormatDateType, Option } from "./types.ts"; 2 | import { Locale } from "./locale.ts"; 3 | import { 4 | formatToThreeDigits, 5 | formatToTwoDigits, 6 | millisecToMin, 7 | weeksOfYear, 8 | } from "./utils.ts"; 9 | import { dateToDayOfYear, dateToTS, dateToWeekDay } from "./convert.ts"; 10 | 11 | export function formatDateObj( 12 | dateObj: DateObj, 13 | formatStr: DateFormatType, 14 | option?: Option, 15 | ): string { 16 | const { year, month, day, hour, minute, second, millisecond } = dateObj; 17 | const twelveHours = (hour || 0) % 12; 18 | 19 | const locale = new Locale(option?.locale ?? "en"); 20 | switch (formatStr) { 21 | case "YY": { 22 | return year.toString().slice(-2); 23 | } 24 | case "YYYY": { 25 | return year.toString(); 26 | } 27 | case "M": { 28 | return month.toString(); 29 | } 30 | case "MM": { 31 | return month <= 9 ? `0${month}` : month.toString(); 32 | } 33 | case "MMM": { 34 | return locale.monthList("short")[month - 1]; 35 | } 36 | case "MMMM": { 37 | return locale.monthList("long")[month - 1]; 38 | } 39 | case "d": { 40 | return day ? day.toString() : "0"; 41 | } 42 | case "dd": { 43 | return day ? formatToTwoDigits(day) : "00"; 44 | } 45 | case "D": { 46 | return dateToDayOfYear(dateObj).toString(); 47 | } 48 | case "DDD": { 49 | return formatToThreeDigits(dateToDayOfYear(dateObj)); 50 | } 51 | case "H": { 52 | return String(hour); 53 | } 54 | case "HH": { 55 | return hour ? formatToTwoDigits(hour) : "00"; 56 | } 57 | case "h": { 58 | return (twelveHours || 12).toString(); 59 | } 60 | case "hh": { 61 | return formatToTwoDigits(twelveHours || 12).toString(); 62 | } 63 | case "m": { 64 | return minute ? minute.toString() : "0"; 65 | } 66 | case "mm": { 67 | return minute ? formatToTwoDigits(minute).toString() : "00"; 68 | } 69 | case "s": { 70 | return second ? second.toString() : "0"; 71 | } 72 | case "ss": { 73 | return second ? formatToTwoDigits(second).toString() : "00"; 74 | } 75 | case "S": { 76 | return millisecond 77 | ? millisecond <= 99 ? "0${millisecond}" : millisecond.toString() 78 | : "000"; 79 | } 80 | case "w": { 81 | return dateToWeekDay(dateObj).toString(); 82 | } 83 | case "www": { 84 | return locale.weekList("short")[dateToWeekDay(dateObj)]; 85 | } 86 | case "wwww": { 87 | return locale.weekList("long")[dateToWeekDay(dateObj)]; 88 | } 89 | case "W": { 90 | return isoWeekNumber(dateObj).toString(); 91 | } 92 | case "WW": { 93 | return formatToTwoDigits(isoWeekNumber(dateObj)); 94 | } 95 | case "a": { 96 | return (hour || 0) / 12 <= 1 ? "AM" : "PM"; 97 | } 98 | case "X": { 99 | return (dateToTS(dateObj) / 1000).toString(); 100 | } 101 | case "x": { 102 | return dateToTS(dateObj).toString(); 103 | } 104 | case "z": { 105 | return option?.timezone ?? ""; 106 | } 107 | case "Z": { 108 | return option?.offsetMillisec 109 | ? formatOffsetMillisec(option.offsetMillisec, "Z") 110 | : ""; 111 | } 112 | case "ZZ": { 113 | return option?.offsetMillisec 114 | ? formatOffsetMillisec(option.offsetMillisec, "ZZ") 115 | : ""; 116 | } 117 | case "ZZZ": { 118 | return locale.offsetName( 119 | new Date(dateToTS(dateObj)), 120 | "short", 121 | option?.timezone, 122 | ) ?? ""; 123 | } 124 | case "ZZZZ": { 125 | return locale.offsetName( 126 | new Date(dateToTS(dateObj)), 127 | "long", 128 | option?.timezone, 129 | ) ?? ""; 130 | } 131 | default: { 132 | throw new TypeError("Please input valid format."); 133 | } 134 | } 135 | } 136 | 137 | function formatOffsetMillisec(offsetMillisec: number, format: "Z" | "ZZ") { 138 | const offsetMin = millisecToMin(offsetMillisec); 139 | const hour = Math.floor(Math.abs(offsetMin) / 60); 140 | const min = Math.abs(offsetMin) % 60; 141 | const sign = offsetMin >= 0 ? "+" : "-"; 142 | 143 | switch (format) { 144 | case "Z": 145 | return `${sign}${formatToTwoDigits(hour)}:${formatToTwoDigits(min)}`; 146 | case "ZZ": 147 | return `${sign}${formatToTwoDigits(hour)}${formatToTwoDigits(min)}`; 148 | default: 149 | throw new TypeError("Please input valid offset format."); 150 | } 151 | } 152 | 153 | function parseFormat( 154 | format: string, 155 | ): { value: string; isLiteral: boolean }[] { 156 | const result = []; 157 | 158 | let currentValue = ""; 159 | let previousChar = null; 160 | let isLiteral = false; 161 | 162 | for (const char of format) { 163 | if (char === "'") { 164 | if (currentValue !== "") { 165 | result.push({ value: currentValue, isLiteral: isLiteral }); 166 | } 167 | currentValue = ""; 168 | previousChar = null; 169 | isLiteral = !isLiteral; 170 | } else if (isLiteral) { 171 | currentValue += char; 172 | } else if (char === previousChar) { 173 | currentValue += char; 174 | } else { 175 | if (currentValue !== "") { 176 | result.push({ value: currentValue, isLiteral: isLiteral }); 177 | } 178 | currentValue = char; 179 | previousChar = char; 180 | } 181 | } 182 | 183 | if (currentValue !== "") { 184 | result.push({ value: currentValue, isLiteral: isLiteral }); 185 | } 186 | 187 | return result; 188 | } 189 | 190 | export function formatDate( 191 | dateObj: DateObj, 192 | formatStr: string, 193 | option?: Option, 194 | ) { 195 | const parsedFormat = parseFormat(formatStr); 196 | let result = ""; 197 | 198 | for (const f of parsedFormat) { 199 | if (f.isLiteral) { 200 | result += f.value; 201 | } else if (isFormatDateType(f.value)) { 202 | result += formatDateObj(dateObj, f.value, option); 203 | } else { 204 | result += f.value; 205 | } 206 | } 207 | return result; 208 | } 209 | 210 | function isoWeekNumber(dateObj: DateObj) { 211 | const ordinalDate = dateToDayOfYear(dateObj); 212 | const weekIndex = dateToWeekDay(dateObj); 213 | 214 | const weekNumber = Math.floor((ordinalDate - weekIndex + 10) / 7); 215 | 216 | if (weekNumber < 1) return weeksOfYear(dateObj.year - 1); 217 | if (weekNumber > weeksOfYear(dateObj.year)) return 1; 218 | 219 | return weekNumber; 220 | } 221 | -------------------------------------------------------------------------------- /format_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 2 | import { formatDate, formatDateObj } from "./format.ts"; 3 | import { Locale } from "./locale.ts"; 4 | import { DateObj, Option, Timezone } from "./types.ts"; 5 | 6 | const defaultLocale = new Locale("en"); 7 | Deno.test("format: YY", () => { 8 | const tests = [ 9 | { 10 | input: { 11 | year: 2021, 12 | month: 1, 13 | day: 1, 14 | hour: 0, 15 | minute: 0, 16 | second: 0, 17 | millisecond: 0, 18 | }, 19 | expected: "21", 20 | }, 21 | { 22 | input: { 23 | year: 1900, 24 | month: 3, 25 | day: 1, 26 | hour: 0, 27 | minute: 0, 28 | second: 0, 29 | millisecond: 0, 30 | }, 31 | expected: "00", 32 | }, 33 | ]; 34 | tests.forEach((t) => { 35 | assertEquals(formatDateObj(t.input, "YY", defaultLocale), t.expected); 36 | }); 37 | }); 38 | 39 | Deno.test("format: YYYY", () => { 40 | const tests = [ 41 | { 42 | input: { 43 | year: 2021, 44 | month: 1, 45 | day: 1, 46 | hour: 0, 47 | minute: 0, 48 | second: 0, 49 | millisecond: 0, 50 | }, 51 | expected: "2021", 52 | }, 53 | { 54 | input: { 55 | year: 1900, 56 | month: 3, 57 | day: 1, 58 | hour: 0, 59 | minute: 0, 60 | second: 0, 61 | millisecond: 0, 62 | }, 63 | expected: "1900", 64 | }, 65 | ]; 66 | tests.forEach((t) => { 67 | assertEquals(formatDateObj(t.input, "YYYY", defaultLocale), t.expected); 68 | }); 69 | }); 70 | 71 | Deno.test("format: M", () => { 72 | const tests = [ 73 | { 74 | input: { 75 | year: 2021, 76 | month: 1, 77 | day: 1, 78 | hour: 0, 79 | minute: 0, 80 | second: 0, 81 | millisecond: 0, 82 | }, 83 | expected: "1", 84 | }, 85 | { 86 | input: { 87 | year: 2021, 88 | month: 8, 89 | day: 1, 90 | hour: 0, 91 | minute: 0, 92 | second: 0, 93 | millisecond: 0, 94 | }, 95 | expected: "8", 96 | }, 97 | { 98 | input: { 99 | year: 2021, 100 | month: 11, 101 | day: 15, 102 | hour: 0, 103 | minute: 0, 104 | second: 0, 105 | millisecond: 0, 106 | }, 107 | expected: "11", 108 | }, 109 | ]; 110 | tests.forEach((t) => { 111 | assertEquals(formatDateObj(t.input, "M", defaultLocale), t.expected); 112 | }); 113 | }); 114 | 115 | Deno.test("format: MM", () => { 116 | const tests = [ 117 | { 118 | input: { 119 | year: 2021, 120 | month: 1, 121 | day: 1, 122 | hour: 0, 123 | minute: 0, 124 | second: 0, 125 | millisecond: 0, 126 | }, 127 | expected: "01", 128 | }, 129 | { 130 | input: { 131 | year: 2021, 132 | month: 8, 133 | day: 1, 134 | hour: 0, 135 | minute: 0, 136 | second: 0, 137 | millisecond: 0, 138 | }, 139 | expected: "08", 140 | }, 141 | { 142 | input: { 143 | year: 2021, 144 | month: 11, 145 | day: 15, 146 | hour: 0, 147 | minute: 0, 148 | second: 0, 149 | millisecond: 0, 150 | }, 151 | expected: "11", 152 | }, 153 | ]; 154 | tests.forEach((t) => { 155 | assertEquals(formatDateObj(t.input, "MM", defaultLocale), t.expected); 156 | }); 157 | }); 158 | 159 | Deno.test("format: MMM", () => { 160 | const tests = [ 161 | { 162 | input: { 163 | year: 2021, 164 | month: 1, 165 | day: 1, 166 | hour: 0, 167 | minute: 0, 168 | second: 0, 169 | millisecond: 0, 170 | }, 171 | expected: "Jan", 172 | }, 173 | { 174 | input: { 175 | year: 2021, 176 | month: 8, 177 | day: 1, 178 | hour: 0, 179 | minute: 0, 180 | second: 0, 181 | millisecond: 0, 182 | }, 183 | expected: "Aug", 184 | }, 185 | { 186 | input: { 187 | year: 2021, 188 | month: 11, 189 | day: 15, 190 | hour: 0, 191 | minute: 0, 192 | second: 0, 193 | millisecond: 0, 194 | }, 195 | expected: "Nov", 196 | }, 197 | ]; 198 | tests.forEach((t) => { 199 | assertEquals(formatDateObj(t.input, "MMM", defaultLocale), t.expected); 200 | }); 201 | }); 202 | 203 | Deno.test("format: MMM ja", () => { 204 | const tests = [ 205 | { 206 | input: { 207 | year: 2021, 208 | month: 1, 209 | day: 1, 210 | hour: 0, 211 | minute: 0, 212 | second: 0, 213 | millisecond: 0, 214 | }, 215 | expected: "1月", 216 | }, 217 | { 218 | input: { 219 | year: 2021, 220 | month: 8, 221 | day: 1, 222 | hour: 0, 223 | minute: 0, 224 | second: 0, 225 | millisecond: 0, 226 | }, 227 | expected: "8月", 228 | }, 229 | { 230 | input: { 231 | year: 2021, 232 | month: 11, 233 | day: 15, 234 | hour: 0, 235 | minute: 0, 236 | second: 0, 237 | millisecond: 0, 238 | }, 239 | expected: "11月", 240 | }, 241 | ]; 242 | const locale = new Locale("ja"); 243 | tests.forEach((t) => { 244 | assertEquals(formatDateObj(t.input, "MMM", locale), t.expected); 245 | }); 246 | }); 247 | 248 | Deno.test("format: MMMM", () => { 249 | const tests = [ 250 | { 251 | input: { 252 | year: 2021, 253 | month: 1, 254 | day: 1, 255 | hour: 0, 256 | minute: 0, 257 | second: 0, 258 | millisecond: 0, 259 | }, 260 | expected: "January", 261 | }, 262 | { 263 | input: { 264 | year: 2021, 265 | month: 8, 266 | day: 1, 267 | hour: 0, 268 | minute: 0, 269 | second: 0, 270 | millisecond: 0, 271 | }, 272 | expected: "August", 273 | }, 274 | { 275 | input: { 276 | year: 2021, 277 | month: 11, 278 | day: 15, 279 | hour: 0, 280 | minute: 0, 281 | second: 0, 282 | millisecond: 0, 283 | }, 284 | expected: "November", 285 | }, 286 | ]; 287 | tests.forEach((t) => { 288 | assertEquals(formatDateObj(t.input, "MMMM", defaultLocale), t.expected); 289 | }); 290 | }); 291 | 292 | Deno.test("format: MMMM ja", () => { 293 | const tests = [ 294 | { 295 | input: { 296 | year: 2021, 297 | month: 1, 298 | day: 1, 299 | hour: 0, 300 | minute: 0, 301 | second: 0, 302 | millisecond: 0, 303 | }, 304 | expected: "1月", 305 | }, 306 | { 307 | input: { 308 | year: 2021, 309 | month: 8, 310 | day: 1, 311 | hour: 0, 312 | minute: 0, 313 | second: 0, 314 | millisecond: 0, 315 | }, 316 | expected: "8月", 317 | }, 318 | { 319 | input: { 320 | year: 2021, 321 | month: 11, 322 | day: 15, 323 | hour: 0, 324 | minute: 0, 325 | second: 0, 326 | millisecond: 0, 327 | }, 328 | expected: "11月", 329 | }, 330 | ]; 331 | const locale = new Locale("ja"); 332 | tests.forEach((t) => { 333 | assertEquals(formatDateObj(t.input, "MMMM", locale), t.expected); 334 | }); 335 | }); 336 | 337 | Deno.test("format: d", () => { 338 | const tests = [ 339 | { 340 | input: { 341 | year: 2021, 342 | month: 1, 343 | day: 1, 344 | hour: 0, 345 | minute: 0, 346 | second: 0, 347 | millisecond: 0, 348 | }, 349 | expected: "1", 350 | }, 351 | { 352 | input: { 353 | year: 2021, 354 | month: 11, 355 | day: 15, 356 | hour: 0, 357 | minute: 0, 358 | second: 0, 359 | millisecond: 0, 360 | }, 361 | expected: "15", 362 | }, 363 | ]; 364 | tests.forEach((t) => { 365 | assertEquals(formatDateObj(t.input, "d", defaultLocale), t.expected); 366 | }); 367 | }); 368 | 369 | Deno.test("format: dd", () => { 370 | const tests = [ 371 | { 372 | input: { 373 | year: 2021, 374 | month: 1, 375 | day: 1, 376 | hour: 0, 377 | minute: 0, 378 | second: 0, 379 | millisecond: 0, 380 | }, 381 | expected: "01", 382 | }, 383 | { 384 | input: { 385 | year: 2021, 386 | month: 11, 387 | day: 15, 388 | hour: 0, 389 | minute: 0, 390 | second: 0, 391 | millisecond: 0, 392 | }, 393 | expected: "15", 394 | }, 395 | ]; 396 | tests.forEach((t) => { 397 | assertEquals(formatDateObj(t.input, "dd", defaultLocale), t.expected); 398 | }); 399 | }); 400 | 401 | Deno.test("format: D", () => { 402 | const tests = [ 403 | { 404 | input: { 405 | year: 2021, 406 | month: 1, 407 | day: 1, 408 | hour: 21, 409 | minute: 0, 410 | second: 0, 411 | millisecond: 0, 412 | }, 413 | expected: "1", 414 | }, 415 | { 416 | input: { 417 | year: 2021, 418 | month: 12, 419 | day: 31, 420 | hour: 21, 421 | minute: 0, 422 | second: 0, 423 | millisecond: 0, 424 | }, 425 | expected: "365", 426 | }, 427 | { 428 | input: { 429 | year: 2020, 430 | month: 12, 431 | day: 31, 432 | hour: 21, 433 | minute: 0, 434 | second: 0, 435 | millisecond: 0, 436 | }, 437 | expected: "366", 438 | }, 439 | ]; 440 | tests.forEach((t) => { 441 | assertEquals(formatDateObj(t.input, "D", defaultLocale), t.expected); 442 | }); 443 | }); 444 | 445 | Deno.test("format: DDD", () => { 446 | const tests = [ 447 | { 448 | input: { 449 | year: 2021, 450 | month: 1, 451 | day: 1, 452 | hour: 0, 453 | minute: 0, 454 | second: 0, 455 | millisecond: 0, 456 | }, 457 | expected: "001", 458 | }, 459 | { 460 | input: { 461 | year: 2021, 462 | month: 2, 463 | day: 23, 464 | hour: 0, 465 | minute: 0, 466 | second: 0, 467 | millisecond: 0, 468 | }, 469 | expected: "054", 470 | }, 471 | { 472 | input: { 473 | year: 2021, 474 | month: 12, 475 | day: 31, 476 | hour: 21, 477 | minute: 0, 478 | second: 0, 479 | millisecond: 0, 480 | }, 481 | expected: "365", 482 | }, 483 | { 484 | input: { 485 | year: 2020, 486 | month: 12, 487 | day: 31, 488 | hour: 21, 489 | minute: 0, 490 | second: 0, 491 | millisecond: 0, 492 | }, 493 | expected: "366", 494 | }, 495 | ]; 496 | tests.forEach((t) => { 497 | assertEquals(formatDateObj(t.input, "DDD", defaultLocale), t.expected); 498 | }); 499 | }); 500 | 501 | Deno.test("format: H", () => { 502 | const tests = [ 503 | { 504 | input: { 505 | year: 2021, 506 | month: 1, 507 | day: 1, 508 | hour: 4, 509 | minute: 0, 510 | second: 0, 511 | millisecond: 0, 512 | }, 513 | expected: "4", 514 | }, 515 | { 516 | input: { 517 | year: 2021, 518 | month: 12, 519 | day: 31, 520 | hour: 9, 521 | minute: 0, 522 | second: 0, 523 | millisecond: 0, 524 | }, 525 | expected: "9", 526 | }, 527 | { 528 | input: { 529 | year: 2020, 530 | month: 12, 531 | day: 31, 532 | hour: 21, 533 | minute: 0, 534 | second: 0, 535 | millisecond: 0, 536 | }, 537 | expected: "21", 538 | }, 539 | { 540 | input: { 541 | year: 2020, 542 | month: 12, 543 | day: 31, 544 | hour: 23, 545 | minute: 0, 546 | second: 0, 547 | millisecond: 0, 548 | }, 549 | expected: "23", 550 | }, 551 | ]; 552 | tests.forEach((t) => { 553 | assertEquals(formatDateObj(t.input, "H", defaultLocale), t.expected); 554 | }); 555 | }); 556 | 557 | Deno.test("format: HH", () => { 558 | const tests = [ 559 | { 560 | input: { 561 | year: 2021, 562 | month: 1, 563 | day: 1, 564 | hour: 4, 565 | minute: 0, 566 | second: 0, 567 | millisecond: 0, 568 | }, 569 | expected: "04", 570 | }, 571 | { 572 | input: { 573 | year: 2021, 574 | month: 12, 575 | day: 31, 576 | hour: 9, 577 | minute: 0, 578 | second: 0, 579 | millisecond: 0, 580 | }, 581 | expected: "09", 582 | }, 583 | { 584 | input: { 585 | year: 2020, 586 | month: 12, 587 | day: 31, 588 | hour: 21, 589 | minute: 0, 590 | second: 0, 591 | millisecond: 0, 592 | }, 593 | expected: "21", 594 | }, 595 | { 596 | input: { 597 | year: 2020, 598 | month: 12, 599 | day: 31, 600 | hour: 23, 601 | minute: 0, 602 | second: 0, 603 | millisecond: 0, 604 | }, 605 | expected: "23", 606 | }, 607 | ]; 608 | tests.forEach((t) => { 609 | assertEquals(formatDateObj(t.input, "HH", defaultLocale), t.expected); 610 | }); 611 | }); 612 | 613 | Deno.test("format: h", () => { 614 | const tests = [ 615 | { 616 | input: { 617 | year: 2021, 618 | month: 1, 619 | day: 1, 620 | hour: 4, 621 | minute: 0, 622 | second: 0, 623 | millisecond: 0, 624 | }, 625 | expected: "4", 626 | }, 627 | { 628 | input: { 629 | year: 2021, 630 | month: 12, 631 | day: 31, 632 | hour: 9, 633 | minute: 0, 634 | second: 0, 635 | millisecond: 0, 636 | }, 637 | expected: "9", 638 | }, 639 | { 640 | input: { 641 | year: 2020, 642 | month: 12, 643 | day: 31, 644 | hour: 21, 645 | minute: 0, 646 | second: 0, 647 | millisecond: 0, 648 | }, 649 | expected: "9", 650 | }, 651 | { 652 | input: { 653 | year: 2020, 654 | month: 12, 655 | day: 31, 656 | hour: 23, 657 | minute: 0, 658 | second: 0, 659 | millisecond: 0, 660 | }, 661 | expected: "11", 662 | }, 663 | ]; 664 | tests.forEach((t) => { 665 | assertEquals(formatDateObj(t.input, "h", defaultLocale), t.expected); 666 | }); 667 | }); 668 | 669 | Deno.test("format: hh", () => { 670 | const tests = [ 671 | { 672 | input: { 673 | year: 2021, 674 | month: 1, 675 | day: 1, 676 | hour: 4, 677 | minute: 0, 678 | second: 0, 679 | millisecond: 0, 680 | }, 681 | expected: "04", 682 | }, 683 | { 684 | input: { 685 | year: 2021, 686 | month: 12, 687 | day: 31, 688 | hour: 9, 689 | minute: 0, 690 | second: 0, 691 | millisecond: 0, 692 | }, 693 | expected: "09", 694 | }, 695 | { 696 | input: { 697 | year: 2020, 698 | month: 12, 699 | day: 31, 700 | hour: 21, 701 | minute: 0, 702 | second: 0, 703 | millisecond: 0, 704 | }, 705 | expected: "09", 706 | }, 707 | { 708 | input: { 709 | year: 2020, 710 | month: 12, 711 | day: 31, 712 | hour: 23, 713 | minute: 0, 714 | second: 0, 715 | millisecond: 0, 716 | }, 717 | expected: "11", 718 | }, 719 | ]; 720 | tests.forEach((t) => { 721 | assertEquals(formatDateObj(t.input, "hh", defaultLocale), t.expected); 722 | }); 723 | }); 724 | 725 | Deno.test("format: m", () => { 726 | const tests = [ 727 | { 728 | input: { 729 | year: 2021, 730 | month: 1, 731 | day: 1, 732 | hour: 1, 733 | minute: 0, 734 | second: 0, 735 | millisecond: 0, 736 | }, 737 | expected: "0", 738 | }, 739 | { 740 | input: { 741 | year: 2021, 742 | month: 12, 743 | day: 31, 744 | hour: 1, 745 | minute: 1, 746 | second: 0, 747 | millisecond: 0, 748 | }, 749 | expected: "1", 750 | }, 751 | { 752 | input: { 753 | year: 2020, 754 | month: 12, 755 | day: 31, 756 | hour: 1, 757 | minute: 10, 758 | second: 0, 759 | millisecond: 0, 760 | }, 761 | expected: "10", 762 | }, 763 | { 764 | input: { 765 | year: 2020, 766 | month: 12, 767 | day: 31, 768 | hour: 1, 769 | minute: 59, 770 | second: 0, 771 | millisecond: 0, 772 | }, 773 | expected: "59", 774 | }, 775 | ]; 776 | tests.forEach((t) => { 777 | assertEquals(formatDateObj(t.input, "m", defaultLocale), t.expected); 778 | }); 779 | }); 780 | 781 | Deno.test("format: mm", () => { 782 | const tests = [ 783 | { 784 | input: { 785 | year: 2021, 786 | month: 1, 787 | day: 1, 788 | hour: 1, 789 | minute: 0, 790 | second: 0, 791 | millisecond: 0, 792 | }, 793 | expected: "00", 794 | }, 795 | { 796 | input: { 797 | year: 2021, 798 | month: 12, 799 | day: 31, 800 | hour: 1, 801 | minute: 1, 802 | second: 0, 803 | millisecond: 0, 804 | }, 805 | expected: "01", 806 | }, 807 | { 808 | input: { 809 | year: 2020, 810 | month: 12, 811 | day: 31, 812 | hour: 1, 813 | minute: 10, 814 | second: 0, 815 | millisecond: 0, 816 | }, 817 | expected: "10", 818 | }, 819 | { 820 | input: { 821 | year: 2020, 822 | month: 12, 823 | day: 31, 824 | hour: 1, 825 | minute: 59, 826 | second: 0, 827 | millisecond: 0, 828 | }, 829 | expected: "59", 830 | }, 831 | ]; 832 | tests.forEach((t) => { 833 | assertEquals(formatDateObj(t.input, "mm", defaultLocale), t.expected); 834 | }); 835 | }); 836 | 837 | Deno.test("format: a", () => { 838 | const tests = [ 839 | { 840 | input: { 841 | year: 2021, 842 | month: 1, 843 | day: 1, 844 | hour: 4, 845 | minute: 0, 846 | second: 0, 847 | millisecond: 0, 848 | }, 849 | expected: "AM", 850 | }, 851 | { 852 | input: { 853 | year: 2021, 854 | month: 12, 855 | day: 31, 856 | hour: 12, 857 | minute: 0, 858 | second: 0, 859 | millisecond: 0, 860 | }, 861 | expected: "AM", 862 | }, 863 | { 864 | input: { 865 | year: 2020, 866 | month: 12, 867 | day: 31, 868 | hour: 13, 869 | minute: 0, 870 | second: 0, 871 | millisecond: 0, 872 | }, 873 | expected: "PM", 874 | }, 875 | { 876 | input: { 877 | year: 2020, 878 | month: 12, 879 | day: 31, 880 | hour: 23, 881 | minute: 0, 882 | second: 0, 883 | millisecond: 0, 884 | }, 885 | expected: "PM", 886 | }, 887 | ]; 888 | tests.forEach((t) => { 889 | assertEquals(formatDateObj(t.input, "a", defaultLocale), t.expected); 890 | }); 891 | }); 892 | 893 | Deno.test("format: w", () => { 894 | const tests = [ 895 | { 896 | input: { 897 | year: 2021, 898 | month: 1, 899 | day: 2, 900 | hour: 0, 901 | minute: 0, 902 | second: 0, 903 | millisecond: 0, 904 | }, 905 | expected: "6", 906 | }, 907 | { 908 | input: { 909 | year: 2021, 910 | month: 1, 911 | day: 3, 912 | hour: 0, 913 | minute: 0, 914 | second: 0, 915 | millisecond: 0, 916 | }, 917 | expected: "0", 918 | }, 919 | { 920 | input: { 921 | year: 2021, 922 | month: 5, 923 | day: 3, 924 | hour: 0, 925 | minute: 0, 926 | second: 0, 927 | millisecond: 0, 928 | }, 929 | expected: "1", 930 | }, 931 | { 932 | input: { 933 | year: 2021, 934 | month: 5, 935 | day: 7, 936 | hour: 0, 937 | minute: 0, 938 | second: 0, 939 | millisecond: 0, 940 | }, 941 | expected: "5", 942 | }, 943 | ]; 944 | tests.forEach((t) => { 945 | assertEquals(formatDateObj(t.input, "w", defaultLocale), t.expected); 946 | }); 947 | }); 948 | 949 | Deno.test("format: www", () => { 950 | const tests = [ 951 | { 952 | input: { 953 | year: 2021, 954 | month: 1, 955 | day: 2, 956 | hour: 0, 957 | minute: 0, 958 | second: 0, 959 | millisecond: 0, 960 | }, 961 | expected: "Sat", 962 | }, 963 | { 964 | input: { 965 | year: 2021, 966 | month: 1, 967 | day: 3, 968 | hour: 0, 969 | minute: 0, 970 | second: 0, 971 | millisecond: 0, 972 | }, 973 | expected: "Sun", 974 | }, 975 | { 976 | input: { 977 | year: 2021, 978 | month: 5, 979 | day: 3, 980 | hour: 0, 981 | minute: 0, 982 | second: 0, 983 | millisecond: 0, 984 | }, 985 | expected: "Mon", 986 | }, 987 | { 988 | input: { 989 | year: 2021, 990 | month: 5, 991 | day: 7, 992 | hour: 0, 993 | minute: 0, 994 | second: 0, 995 | millisecond: 0, 996 | }, 997 | expected: "Fri", 998 | }, 999 | ]; 1000 | tests.forEach((t) => { 1001 | assertEquals(formatDateObj(t.input, "www", defaultLocale), t.expected); 1002 | }); 1003 | }); 1004 | 1005 | Deno.test("format: www ja", () => { 1006 | const tests = [ 1007 | { 1008 | input: { 1009 | year: 2021, 1010 | month: 1, 1011 | day: 2, 1012 | hour: 0, 1013 | minute: 0, 1014 | second: 0, 1015 | millisecond: 0, 1016 | }, 1017 | expected: "土", 1018 | }, 1019 | { 1020 | input: { 1021 | year: 2021, 1022 | month: 1, 1023 | day: 3, 1024 | hour: 0, 1025 | minute: 0, 1026 | second: 0, 1027 | millisecond: 0, 1028 | }, 1029 | expected: "日", 1030 | }, 1031 | { 1032 | input: { 1033 | year: 2021, 1034 | month: 5, 1035 | day: 3, 1036 | hour: 0, 1037 | minute: 0, 1038 | second: 0, 1039 | millisecond: 0, 1040 | }, 1041 | expected: "月", 1042 | }, 1043 | { 1044 | input: { 1045 | year: 2021, 1046 | month: 5, 1047 | day: 7, 1048 | hour: 0, 1049 | minute: 0, 1050 | second: 0, 1051 | millisecond: 0, 1052 | }, 1053 | expected: "金", 1054 | }, 1055 | ]; 1056 | const locale = new Locale("ja"); 1057 | tests.forEach((t) => { 1058 | assertEquals(formatDateObj(t.input, "www", locale), t.expected); 1059 | }); 1060 | }); 1061 | 1062 | Deno.test("format: wwww", () => { 1063 | const tests = [ 1064 | { 1065 | input: { 1066 | year: 2021, 1067 | month: 1, 1068 | day: 2, 1069 | hour: 0, 1070 | minute: 0, 1071 | second: 0, 1072 | millisecond: 0, 1073 | }, 1074 | expected: "Saturday", 1075 | }, 1076 | { 1077 | input: { 1078 | year: 2021, 1079 | month: 1, 1080 | day: 3, 1081 | hour: 0, 1082 | minute: 0, 1083 | second: 0, 1084 | millisecond: 0, 1085 | }, 1086 | expected: "Sunday", 1087 | }, 1088 | { 1089 | input: { 1090 | year: 2021, 1091 | month: 5, 1092 | day: 3, 1093 | hour: 0, 1094 | minute: 0, 1095 | second: 0, 1096 | millisecond: 0, 1097 | }, 1098 | expected: "Monday", 1099 | }, 1100 | { 1101 | input: { 1102 | year: 2021, 1103 | month: 5, 1104 | day: 7, 1105 | hour: 0, 1106 | minute: 0, 1107 | second: 0, 1108 | millisecond: 0, 1109 | }, 1110 | expected: "Friday", 1111 | }, 1112 | ]; 1113 | tests.forEach((t) => { 1114 | assertEquals(formatDateObj(t.input, "wwww", defaultLocale), t.expected); 1115 | }); 1116 | }); 1117 | 1118 | Deno.test("format: wwww ja", () => { 1119 | const tests = [ 1120 | { 1121 | input: { 1122 | year: 2021, 1123 | month: 1, 1124 | day: 2, 1125 | hour: 0, 1126 | minute: 0, 1127 | second: 0, 1128 | millisecond: 0, 1129 | }, 1130 | expected: "土曜日", 1131 | }, 1132 | { 1133 | input: { 1134 | year: 2021, 1135 | month: 1, 1136 | day: 3, 1137 | hour: 0, 1138 | minute: 0, 1139 | second: 0, 1140 | millisecond: 0, 1141 | }, 1142 | expected: "日曜日", 1143 | }, 1144 | { 1145 | input: { 1146 | year: 2021, 1147 | month: 5, 1148 | day: 3, 1149 | hour: 0, 1150 | minute: 0, 1151 | second: 0, 1152 | millisecond: 0, 1153 | }, 1154 | expected: "月曜日", 1155 | }, 1156 | { 1157 | input: { 1158 | year: 2021, 1159 | month: 5, 1160 | day: 7, 1161 | hour: 0, 1162 | minute: 0, 1163 | second: 0, 1164 | millisecond: 0, 1165 | }, 1166 | expected: "金曜日", 1167 | }, 1168 | ]; 1169 | const locale = new Locale("ja"); 1170 | tests.forEach((t) => { 1171 | assertEquals(formatDateObj(t.input, "wwww", locale), t.expected); 1172 | }); 1173 | }); 1174 | 1175 | Deno.test("format: W", () => { 1176 | const tests = [ 1177 | { 1178 | input: { 1179 | year: 2021, 1180 | month: 1, 1181 | day: 1, 1182 | hour: 0, 1183 | minute: 0, 1184 | second: 0, 1185 | millisecond: 0, 1186 | }, 1187 | expected: "53", 1188 | }, 1189 | { 1190 | input: { 1191 | year: 2021, 1192 | month: 1, 1193 | day: 4, 1194 | hour: 0, 1195 | minute: 0, 1196 | second: 0, 1197 | millisecond: 0, 1198 | }, 1199 | expected: "1", 1200 | }, 1201 | { 1202 | input: { 1203 | year: 2021, 1204 | month: 5, 1205 | day: 25, 1206 | hour: 0, 1207 | minute: 0, 1208 | second: 0, 1209 | millisecond: 0, 1210 | }, 1211 | expected: "21", 1212 | }, 1213 | { 1214 | input: { 1215 | year: 2021, 1216 | month: 11, 1217 | day: 4, 1218 | hour: 0, 1219 | minute: 0, 1220 | second: 0, 1221 | millisecond: 0, 1222 | }, 1223 | expected: "44", 1224 | }, 1225 | { 1226 | input: { 1227 | year: 2021, 1228 | month: 12, 1229 | day: 31, 1230 | hour: 0, 1231 | minute: 0, 1232 | second: 0, 1233 | millisecond: 0, 1234 | }, 1235 | expected: "52", 1236 | }, 1237 | { 1238 | input: { 1239 | year: 2020, 1240 | month: 12, 1241 | day: 31, 1242 | hour: 0, 1243 | minute: 0, 1244 | second: 0, 1245 | millisecond: 0, 1246 | }, 1247 | expected: "53", 1248 | }, 1249 | ]; 1250 | tests.forEach((t) => { 1251 | assertEquals(formatDateObj(t.input, "W", defaultLocale), t.expected); 1252 | }); 1253 | }); 1254 | 1255 | Deno.test("format: WW", () => { 1256 | const tests = [ 1257 | { 1258 | input: { 1259 | year: 2021, 1260 | month: 1, 1261 | day: 1, 1262 | hour: 0, 1263 | minute: 0, 1264 | second: 0, 1265 | millisecond: 0, 1266 | }, 1267 | expected: "53", 1268 | }, 1269 | { 1270 | input: { 1271 | year: 2021, 1272 | month: 1, 1273 | day: 4, 1274 | hour: 0, 1275 | minute: 0, 1276 | second: 0, 1277 | millisecond: 0, 1278 | }, 1279 | expected: "01", 1280 | }, 1281 | { 1282 | input: { 1283 | year: 2021, 1284 | month: 5, 1285 | day: 25, 1286 | hour: 0, 1287 | minute: 0, 1288 | second: 0, 1289 | millisecond: 0, 1290 | }, 1291 | expected: "21", 1292 | }, 1293 | { 1294 | input: { 1295 | year: 2021, 1296 | month: 11, 1297 | day: 4, 1298 | hour: 0, 1299 | minute: 0, 1300 | second: 0, 1301 | millisecond: 0, 1302 | }, 1303 | expected: "44", 1304 | }, 1305 | { 1306 | input: { 1307 | year: 2021, 1308 | month: 12, 1309 | day: 31, 1310 | hour: 0, 1311 | minute: 0, 1312 | second: 0, 1313 | millisecond: 0, 1314 | }, 1315 | expected: "52", 1316 | }, 1317 | { 1318 | input: { 1319 | year: 2020, 1320 | month: 12, 1321 | day: 31, 1322 | hour: 0, 1323 | minute: 0, 1324 | second: 0, 1325 | millisecond: 0, 1326 | }, 1327 | expected: "53", 1328 | }, 1329 | ]; 1330 | tests.forEach((t) => { 1331 | assertEquals(formatDateObj(t.input, "WW", defaultLocale), t.expected); 1332 | }); 1333 | }); 1334 | 1335 | Deno.test("format: X", () => { 1336 | const tests = [ 1337 | { 1338 | input: { 1339 | year: 2021, 1340 | month: 1, 1341 | day: 1, 1342 | hour: 13, 1343 | minute: 30, 1344 | second: 0, 1345 | millisecond: 0, 1346 | }, 1347 | expected: "1609507800", 1348 | }, 1349 | { 1350 | input: { 1351 | year: 2021, 1352 | month: 1, 1353 | day: 4, 1354 | hour: 0, 1355 | minute: 0, 1356 | second: 0, 1357 | millisecond: 0, 1358 | }, 1359 | expected: "1609718400", 1360 | }, 1361 | { 1362 | input: { 1363 | year: 2021, 1364 | month: 5, 1365 | day: 25, 1366 | hour: 0, 1367 | minute: 0, 1368 | second: 0, 1369 | millisecond: 0, 1370 | }, 1371 | expected: "1621900800", 1372 | }, 1373 | ]; 1374 | tests.forEach((t) => { 1375 | assertEquals(formatDateObj(t.input, "X", defaultLocale), t.expected); 1376 | }); 1377 | }); 1378 | 1379 | Deno.test("format: x", () => { 1380 | const tests = [ 1381 | { 1382 | input: { 1383 | year: 2021, 1384 | month: 1, 1385 | day: 1, 1386 | hour: 13, 1387 | minute: 30, 1388 | second: 0, 1389 | millisecond: 0, 1390 | }, 1391 | expected: "1609507800000", 1392 | }, 1393 | { 1394 | input: { 1395 | year: 2021, 1396 | month: 1, 1397 | day: 4, 1398 | hour: 0, 1399 | minute: 0, 1400 | second: 0, 1401 | millisecond: 0, 1402 | }, 1403 | expected: "1609718400000", 1404 | }, 1405 | { 1406 | input: { 1407 | year: 2021, 1408 | month: 5, 1409 | day: 25, 1410 | hour: 0, 1411 | minute: 0, 1412 | second: 0, 1413 | millisecond: 0, 1414 | }, 1415 | expected: "1621900800000", 1416 | }, 1417 | ]; 1418 | tests.forEach((t) => { 1419 | assertEquals(formatDateObj(t.input, "x", defaultLocale), t.expected); 1420 | }); 1421 | }); 1422 | 1423 | Deno.test("format: z", () => { 1424 | const tests = [ 1425 | { 1426 | input: { 1427 | year: 2021, 1428 | month: 1, 1429 | day: 1, 1430 | hour: 0, 1431 | minute: 0, 1432 | second: 0, 1433 | millisecond: 0, 1434 | }, 1435 | timezone: "Asia/Tokyo", 1436 | expected: "Asia/Tokyo", 1437 | }, 1438 | { 1439 | input: { 1440 | year: 2021, 1441 | month: 1, 1442 | day: 4, 1443 | hour: 0, 1444 | minute: 0, 1445 | second: 0, 1446 | millisecond: 0, 1447 | }, 1448 | timezone: "America/New_York", 1449 | expected: "America/New_York", 1450 | }, 1451 | ]; 1452 | tests.forEach((t) => { 1453 | assertEquals( 1454 | formatDateObj(t.input, "z", { timezone: t.timezone as Timezone }), 1455 | t.expected, 1456 | ); 1457 | }); 1458 | }); 1459 | 1460 | Deno.test("format: Z", () => { 1461 | const tests = [ 1462 | { 1463 | input: { 1464 | year: 2021, 1465 | month: 1, 1466 | day: 1, 1467 | hour: 0, 1468 | minute: 0, 1469 | second: 0, 1470 | millisecond: 0, 1471 | }, 1472 | offset: 10800000, 1473 | expected: "+03:00", 1474 | }, 1475 | { 1476 | input: { 1477 | year: 2021, 1478 | month: 1, 1479 | day: 4, 1480 | hour: 0, 1481 | minute: 0, 1482 | second: 0, 1483 | millisecond: 0, 1484 | }, 1485 | offset: 19800000, 1486 | expected: "+05:30", 1487 | }, 1488 | { 1489 | input: { 1490 | year: 2021, 1491 | month: 1, 1492 | day: 4, 1493 | hour: 0, 1494 | minute: 0, 1495 | second: 0, 1496 | millisecond: 0, 1497 | }, 1498 | offset: -19800000, 1499 | expected: "-05:30", 1500 | }, 1501 | ]; 1502 | tests.forEach((t) => { 1503 | assertEquals( 1504 | formatDateObj(t.input, "Z", { offsetMillisec: t.offset }), 1505 | t.expected, 1506 | ); 1507 | }); 1508 | }); 1509 | 1510 | Deno.test("format: ZZ", () => { 1511 | const tests = [ 1512 | { 1513 | input: { 1514 | year: 2021, 1515 | month: 1, 1516 | day: 1, 1517 | hour: 0, 1518 | minute: 0, 1519 | second: 0, 1520 | millisecond: 0, 1521 | }, 1522 | offset: 10800000, 1523 | expected: "+0300", 1524 | }, 1525 | { 1526 | input: { 1527 | year: 2021, 1528 | month: 1, 1529 | day: 4, 1530 | hour: 0, 1531 | minute: 0, 1532 | second: 0, 1533 | millisecond: 0, 1534 | }, 1535 | offset: 19800000, 1536 | expected: "+0530", 1537 | }, 1538 | { 1539 | input: { 1540 | year: 2021, 1541 | month: 1, 1542 | day: 4, 1543 | hour: 0, 1544 | minute: 0, 1545 | second: 0, 1546 | millisecond: 0, 1547 | }, 1548 | offset: -19800000, 1549 | expected: "-0530", 1550 | }, 1551 | ]; 1552 | tests.forEach((t) => { 1553 | assertEquals( 1554 | formatDateObj(t.input, "ZZ", { offsetMillisec: t.offset }), 1555 | t.expected, 1556 | ); 1557 | }); 1558 | }); 1559 | 1560 | Deno.test("format: ZZZ", () => { 1561 | type Test = { 1562 | input: DateObj; 1563 | tz?: Timezone; 1564 | locale: string; 1565 | expected: string; 1566 | }; 1567 | 1568 | const tests: Test[] = [ 1569 | { 1570 | input: { 1571 | year: 2021, 1572 | month: 1, 1573 | day: 1, 1574 | hour: 0, 1575 | minute: 0, 1576 | second: 0, 1577 | millisecond: 0, 1578 | }, 1579 | tz: "Asia/Tokyo", 1580 | locale: "ja", 1581 | expected: "JST", 1582 | }, 1583 | { 1584 | input: { 1585 | year: 2021, 1586 | month: 1, 1587 | day: 4, 1588 | hour: 0, 1589 | minute: 0, 1590 | second: 0, 1591 | millisecond: 0, 1592 | }, 1593 | tz: "America/New_York", 1594 | locale: "fr", 1595 | expected: "UTC−5", 1596 | }, 1597 | ]; 1598 | tests.forEach((t) => { 1599 | assertEquals( 1600 | formatDateObj(t.input, "ZZZ", { locale: t.locale, timezone: t.tz }), 1601 | t.expected, 1602 | ); 1603 | }); 1604 | }); 1605 | 1606 | Deno.test("format: ZZZZ", () => { 1607 | type Test = { 1608 | input: DateObj; 1609 | tz: Timezone; 1610 | locale: string; 1611 | expected: string; 1612 | }; 1613 | 1614 | const tests: Test[] = [ 1615 | { 1616 | input: { 1617 | year: 2021, 1618 | month: 1, 1619 | day: 1, 1620 | hour: 0, 1621 | minute: 0, 1622 | second: 0, 1623 | millisecond: 0, 1624 | }, 1625 | tz: "Asia/Tokyo", 1626 | locale: "ja", 1627 | expected: "日本標準時", 1628 | }, 1629 | { 1630 | input: { 1631 | year: 2021, 1632 | month: 1, 1633 | day: 4, 1634 | hour: 0, 1635 | minute: 0, 1636 | second: 0, 1637 | millisecond: 0, 1638 | }, 1639 | tz: "America/New_York", 1640 | locale: "fr", 1641 | expected: "heure normale de l’Est nord-américain", 1642 | }, 1643 | { 1644 | input: { 1645 | year: 2021, 1646 | month: 1, 1647 | day: 4, 1648 | hour: 0, 1649 | minute: 0, 1650 | second: 0, 1651 | millisecond: 0, 1652 | }, 1653 | tz: "America/New_York", 1654 | locale: "en", 1655 | expected: "Eastern Standard Time", 1656 | }, 1657 | ]; 1658 | tests.forEach((t) => { 1659 | assertEquals( 1660 | formatDateObj(t.input, "ZZZZ", { locale: t.locale, timezone: t.tz }), 1661 | t.expected, 1662 | ); 1663 | }); 1664 | }); 1665 | 1666 | Deno.test("formatDate", () => { 1667 | type Test = { 1668 | input: DateObj; 1669 | formatStr: string; 1670 | option?: Option; 1671 | expected: string; 1672 | }; 1673 | const tests: Test[] = [ 1674 | { 1675 | input: { 1676 | year: 2021, 1677 | month: 6, 1678 | day: 1, 1679 | hour: 9, 1680 | minute: 0, 1681 | second: 0, 1682 | millisecond: 0, 1683 | }, 1684 | formatStr: "MMMM YYYY", 1685 | option: undefined, 1686 | expected: "June 2021", 1687 | }, 1688 | { 1689 | input: { 1690 | year: 2021, 1691 | month: 6, 1692 | day: 1, 1693 | hour: 0, 1694 | minute: 0, 1695 | second: 0, 1696 | millisecond: 0, 1697 | }, 1698 | formatStr: "x YYYY", 1699 | option: undefined, 1700 | expected: "1622505600000 2021", 1701 | }, 1702 | { 1703 | input: { 1704 | year: 2021, 1705 | month: 6, 1706 | day: 1, 1707 | hour: 0, 1708 | minute: 0, 1709 | second: 0, 1710 | millisecond: 0, 1711 | }, 1712 | formatStr: "z MMMM", 1713 | option: { timezone: "Asia/Tokyo" }, 1714 | expected: "Asia/Tokyo June", 1715 | }, 1716 | ]; 1717 | tests.forEach((t) => { 1718 | assertEquals( 1719 | formatDate(t.input, t.formatStr, t.option), 1720 | t.expected, 1721 | ); 1722 | }); 1723 | }); 1724 | -------------------------------------------------------------------------------- /local_time.ts: -------------------------------------------------------------------------------- 1 | import { MILLISECONDS_IN_MINUTE } from "./constants.ts"; 2 | import { DateObj } from "./types.ts"; 3 | import { dateToJSDate, jsDateToDate } from "./convert.ts"; 4 | 5 | export function getLocalName(): string { 6 | return new Intl.DateTimeFormat().resolvedOptions().timeZone; 7 | } 8 | 9 | export function getLocalOffset(dateObj: DateObj): number { 10 | return -dateToJSDate(dateObj).getTimezoneOffset() * 11 | MILLISECONDS_IN_MINUTE; 12 | } 13 | 14 | export function utcToLocalTime(dateObj: DateObj): DateObj { 15 | const ts = dateToJSDate(dateObj).getTime() + 16 | getLocalOffset(dateObj); 17 | return jsDateToDate(new Date(ts)); 18 | } 19 | -------------------------------------------------------------------------------- /locale.ts: -------------------------------------------------------------------------------- 1 | import { Timezone } from "./types.ts"; 2 | 3 | function cacheKey(locale: string, options: unknown): string { 4 | return JSON.stringify([locale, options]); 5 | } 6 | 7 | const dtfCache: Record = {}; 8 | function cachedDTF(locale: string, options: Intl.DateTimeFormatOptions) { 9 | const key = cacheKey(locale, options); 10 | let dtf = dtfCache[key]; 11 | 12 | if (!dtf) { 13 | dtf = new Intl.DateTimeFormat(locale, options); 14 | dtfCache[key] = dtf; 15 | } 16 | return dtf; 17 | } 18 | 19 | const nfCache: Record = {}; 20 | function cachedNF(locale: string, options: Intl.NumberFormatOptions) { 21 | const key = cacheKey(locale, options); 22 | let nf = nfCache[key]; 23 | 24 | if (!nf) { 25 | nf = new Intl.NumberFormat(locale, options); 26 | nfCache[key] = nf; 27 | } 28 | return nf; 29 | } 30 | 31 | const rtfCache: Record = {}; 32 | function cachedRTF(locale: string, options: Intl.RelativeTimeFormatOptions) { 33 | const key = cacheKey(locale, options); 34 | let rtf = rtfCache[key]; 35 | 36 | if (!rtf) { 37 | rtf = new Intl.RelativeTimeFormat(locale, options); 38 | rtfCache[key] = rtf; 39 | } 40 | return rtf; 41 | } 42 | 43 | export class Locale { 44 | readonly locale: string; 45 | readonly dtfOptions: Intl.DateTimeFormatOptions; 46 | readonly nfOptions: Intl.NumberFormatOptions; 47 | readonly rtfOptions: Intl.RelativeTimeFormatOptions; 48 | 49 | constructor( 50 | locale: string, 51 | options?: { 52 | dtfOptions?: Intl.DateTimeFormatOptions; 53 | nfOptions?: Intl.NumberFormatOptions; 54 | rtfOptions?: Intl.RelativeTimeFormatOptions; 55 | }, 56 | ) { 57 | this.locale = locale; 58 | this.dtfOptions = options?.dtfOptions ?? {}; 59 | this.nfOptions = options?.nfOptions ?? {}; 60 | this.rtfOptions = options?.rtfOptions ?? {}; 61 | } 62 | 63 | dtfFormat(date: Date, options?: Intl.DateTimeFormatOptions) { 64 | const opts = options ?? this.dtfOptions; 65 | return cachedDTF(this.locale, opts).format(date); 66 | } 67 | 68 | dtfFormatToParts(date: Date, options?: Intl.DateTimeFormatOptions) { 69 | const opts = options ?? this.dtfOptions; 70 | return cachedDTF(this.locale, opts).formatToParts(date); 71 | } 72 | 73 | nfFormat(n: number, options?: Intl.NumberFormatOptions) { 74 | const opts = options ?? this.nfOptions; 75 | return cachedNF(this.locale, opts).format(n); 76 | } 77 | 78 | nfFormatToParts(n: number, options?: Intl.NumberFormatOptions) { 79 | const opts = options ?? this.nfOptions; 80 | return cachedNF(this.locale, opts).formatToParts(n); 81 | } 82 | 83 | rtfFormat( 84 | n: number, 85 | unit: Intl.RelativeTimeFormatUnit, 86 | options?: Intl.RelativeTimeFormatOptions, 87 | ) { 88 | const opts = options ?? this.rtfOptions; 89 | return cachedRTF(this.locale, opts).format(n, unit); 90 | } 91 | 92 | rtfFormatToParts( 93 | n: number, 94 | unit: Intl.RelativeTimeFormatUnit, 95 | options?: Intl.RelativeTimeFormatOptions, 96 | ) { 97 | const opts = options ?? this.rtfOptions; 98 | return cachedRTF(this.locale, opts).formatToParts(n, unit); 99 | } 100 | 101 | monthList(format: "numeric" | "2-digit" | "long" | "short" | "narrow") { 102 | const monthIndexes = [...Array(12).keys()]; 103 | return monthIndexes.map((m) => 104 | this.dtfFormat(new Date(2021, m), { month: format }) 105 | ); 106 | } 107 | 108 | weekList(format: "long" | "short" | "narrow") { 109 | const weekIndexes = [...Array(7).keys()]; 110 | 111 | return weekIndexes.map((m) => 112 | this.dtfFormat(new Date(2021, 0, 3 + m), { weekday: format }) 113 | ); 114 | } 115 | 116 | meridiems(format?: "long" | "short" | "narrow") { 117 | return [ 118 | this.#extractDTFParts(new Date(Date.UTC(2021, 1, 1, 9)), "dayPeriod", { 119 | dayPeriod: format, 120 | hour12: true, 121 | hour: "numeric", 122 | timeZone: "UTC", 123 | }), 124 | this.#extractDTFParts(new Date(Date.UTC(2021, 1, 1, 21)), "dayPeriod", { 125 | dayPeriod: format, 126 | hour12: true, 127 | hour: "numeric", 128 | timeZone: "UTC", 129 | }), 130 | ]; 131 | } 132 | 133 | eras(format: "long" | "short" | "narrow") { 134 | return [ 135 | this.#extractDTFParts(new Date(-40, 1, 1), "era", { era: format }), 136 | this.#extractDTFParts(new Date(2021, 1, 1), "era", { era: format }), 137 | ]; 138 | } 139 | 140 | offsetName(date: Date, offsetFormat: "long" | "short", timezone?: Timezone) { 141 | const parsedParts = this.#extractDTFParts(date, "timeZoneName", { 142 | hour12: false, 143 | year: "numeric", 144 | month: "2-digit", 145 | day: "2-digit", 146 | hour: "2-digit", 147 | minute: "2-digit", 148 | timeZoneName: offsetFormat, 149 | timeZone: timezone, 150 | }); 151 | return parsedParts; 152 | } 153 | 154 | #extractDTFParts( 155 | date: Date, 156 | type: Intl.DateTimeFormatPartTypes, 157 | options?: Intl.DateTimeFormatOptions, 158 | ) { 159 | const opts = options ?? this.dtfOptions; 160 | const parts = this.dtfFormatToParts(date, opts); 161 | const matchParts = parts.find((p) => p.type === type); 162 | return matchParts?.value ?? null; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /locale_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 2 | import { Locale } from "./locale.ts"; 3 | 4 | Deno.test("meridiems", () => { 5 | const tests = [ 6 | { input: "en", expected: ["AM", "PM"] }, 7 | { input: "ja", expected: ["午前", "午後"] }, 8 | ]; 9 | 10 | tests.forEach((t) => { 11 | assertEquals(new Locale(t.input).meridiems(), t.expected); 12 | }); 13 | }); 14 | 15 | Deno.test("eras", () => { 16 | const tests = [ 17 | { input: "en", expected: ["Before Christ", "Anno Domini"] }, 18 | { input: "ja", expected: ["紀元前", "西暦"] }, 19 | ]; 20 | 21 | tests.forEach((t) => { 22 | assertEquals(new Locale(t.input).eras("long"), t.expected); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { 2 | DateTime, 3 | datetime, 4 | diffInDays, 5 | diffInHours, 6 | diffInMillisec, 7 | diffInMin, 8 | diffInSec, 9 | latestDateTime, 10 | oldestDateTime, 11 | } from "./datetime.ts"; 12 | export type { TIMEZONE, Timezone } from "./types.ts"; 13 | export { 14 | MILLISECONDS_IN_DAY, 15 | MILLISECONDS_IN_HOUR, 16 | MILLISECONDS_IN_MINUTE, 17 | } from "./constants.ts"; 18 | -------------------------------------------------------------------------------- /parse_date.ts: -------------------------------------------------------------------------------- 1 | import { dayOfYearToDate } from "./convert.ts"; 2 | import { Locale } from "./locale.ts"; 3 | import { DateFormatType, DateObj, isFormatDateType } from "./types.ts"; 4 | import { 5 | INVALID_DATE, 6 | isValidDate, 7 | minToMillisec, 8 | parseInteger, 9 | } from "./utils.ts"; 10 | 11 | const formatsRegex = 12 | /([-:/.()\s\_]+)|(YYYY|MMMM|MMM|MM|M|dd?|DDD|D|HH?|hh?|mm?|ss?|S{1,3}|wwww|www|w|ZZ?|a|'.')/g; 13 | 14 | const oneDigitRegex = /\d/; 15 | const fourDigitsRegex = /\d\d\d\d/; 16 | const oneToTwoDigitRegex = /\d\d?/; 17 | const oneToThreeDigitRegex = /\d{1,3}/; 18 | const offsetRegex = /[+-]\d\d:?(\d\d)?|Z/; 19 | const literalRegex = /\d*[^\s\d-_:/()]+/; 20 | 21 | function arrayToRegex(array: string[]) { 22 | return new RegExp(array.join("|"), "g"); 23 | } 24 | 25 | type ParsedFormat = { 26 | regex: RegExp; 27 | property: string; 28 | cursor: number | null; 29 | }; 30 | 31 | function parseFormatStr( 32 | formatStr: DateFormatType, 33 | locale: Locale, 34 | ): ParsedFormat { 35 | switch (formatStr) { 36 | case "YY": 37 | case "YYYY": { 38 | return { 39 | regex: fourDigitsRegex, 40 | property: "year", 41 | cursor: 4, 42 | }; 43 | } 44 | case "M": 45 | case "MM": { 46 | return { 47 | regex: oneToTwoDigitRegex, 48 | property: "month", 49 | cursor: 2, 50 | }; 51 | } 52 | case "MMM": { 53 | return { 54 | regex: arrayToRegex(locale.monthList("short")), 55 | property: "shortMonthStr", 56 | cursor: null, 57 | }; 58 | } 59 | case "MMMM": { 60 | return { 61 | regex: arrayToRegex(locale.monthList("long")), 62 | property: "monthStr", 63 | cursor: null, 64 | }; 65 | } 66 | case "d": 67 | case "dd": { 68 | return { 69 | regex: oneToTwoDigitRegex, 70 | property: "day", 71 | cursor: 2, 72 | }; 73 | } 74 | case "D": 75 | case "DDD": { 76 | return { 77 | regex: oneToThreeDigitRegex, 78 | property: "dayOfYear", 79 | cursor: 3, 80 | }; 81 | } 82 | case "H": 83 | case "HH": 84 | case "h": 85 | case "hh": { 86 | return { 87 | regex: oneToTwoDigitRegex, 88 | property: "hour", 89 | cursor: 2, 90 | }; 91 | } 92 | case "m": 93 | case "mm": { 94 | return { 95 | regex: oneToTwoDigitRegex, 96 | property: "minute", 97 | cursor: 2, 98 | }; 99 | } 100 | case "s": 101 | case "ss": { 102 | return { 103 | regex: oneToTwoDigitRegex, 104 | property: "second", 105 | cursor: 2, 106 | }; 107 | } 108 | case "S": { 109 | return { 110 | regex: oneToThreeDigitRegex, 111 | property: "millisecond", 112 | cursor: 3, 113 | }; 114 | } 115 | case "w": { 116 | return { 117 | regex: oneDigitRegex, 118 | property: "weekDay", 119 | cursor: 1, 120 | }; 121 | } 122 | case "www": { 123 | return { 124 | regex: arrayToRegex(locale.weekList("short")), 125 | property: "week", 126 | cursor: null, 127 | }; 128 | } 129 | case "wwww": { 130 | return { 131 | regex: arrayToRegex(locale.weekList("long")), 132 | property: "week", 133 | cursor: null, 134 | }; 135 | } 136 | case "a": { 137 | return { 138 | regex: literalRegex, 139 | property: "AMPM", 140 | cursor: 2, 141 | }; 142 | } 143 | case "Z": 144 | case "ZZ": { 145 | return { 146 | regex: offsetRegex, 147 | property: "offset", 148 | cursor: 6, 149 | }; 150 | } 151 | default: { 152 | throw new TypeError("Please input valid format."); 153 | } 154 | } 155 | } 156 | 157 | type ParseResult = DateObj & { locale?: string; offsetMillisec?: number }; 158 | 159 | export function parseDateStr( 160 | dateStr: string, 161 | format: string, 162 | option?: { locale: string }, 163 | ): ParseResult { 164 | const locale = new Locale(option?.locale ?? "en"); 165 | const hash = dateStrToHash(dateStr, format, locale); 166 | return hashToDate(hash, locale); 167 | } 168 | 169 | function dateStrToHash( 170 | dateStr: string, 171 | formatStr: string, 172 | locale: Locale, 173 | ): { [key: string]: string } { 174 | const parsedFormat = formatStr.match(formatsRegex); 175 | let cursor = 0; 176 | const hash: { [key: string]: string } = {}; 177 | if (parsedFormat) { 178 | for (const f of parsedFormat) { 179 | if (isFormatDateType(f)) { 180 | const { regex, property, cursor: formatCursor } = parseFormatStr( 181 | f, 182 | locale, 183 | ); 184 | const targetStr = formatCursor 185 | ? dateStr.substr(cursor, formatCursor) 186 | : dateStr.substr(cursor); 187 | const parts = targetStr.match(regex); 188 | if (parts) { 189 | cursor += parts[0].length; 190 | hash[property] = parts[0]; 191 | } else { 192 | return {}; 193 | } 194 | } else if (f.match(/'.'/)) { 195 | cursor += f.length - 2; 196 | } else { 197 | cursor += f.length; 198 | } 199 | } 200 | } 201 | return hash; 202 | } 203 | 204 | function hashToDate( 205 | hash: { [key: string]: string }, 206 | locale: Locale, 207 | ): ParseResult { 208 | const year = parseInteger(hash["year"]); 209 | const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const; 210 | 211 | let month = undefined; 212 | if (hash["monthStr"]) { 213 | month = months[locale.monthList("long").indexOf(hash["monthStr"])]; 214 | } 215 | 216 | if (hash["shortMonthStr"]) { 217 | month = months[locale.monthList("short").indexOf(hash["shortMonthStr"])]; 218 | } 219 | 220 | if (hash["month"]) { 221 | month = parseInteger((hash["month"])); 222 | } 223 | 224 | let day = parseInteger((hash["day"])); 225 | 226 | if (hash["dayOfYear"]) { 227 | const dayOfYear = parseInteger(hash["dayOfYear"]); 228 | const date = dayOfYear && year 229 | ? dayOfYearToDate(dayOfYear, year) 230 | : undefined; 231 | month = date?.month; 232 | day = date?.day; 233 | } 234 | 235 | const hour = parseInteger((hash["hour"])); 236 | const minute = parseInteger((hash["minute"])); 237 | const second = parseInteger((hash["second"])); 238 | const millisecond = parseInteger((hash["millisecond"])); 239 | 240 | if ( 241 | !isValidDate({ year, month, day, hour, minute, second, millisecond }) 242 | ) { 243 | return INVALID_DATE; 244 | } 245 | const offsetMillisec = hash["offset"] 246 | ? parseOffsetMillisec(hash["offset"]) 247 | : null; 248 | const isPM = hash["AMPM"] === "PM"; 249 | 250 | return { 251 | year: year as number, 252 | month: month ?? 0, 253 | day: day ?? 0, 254 | hour: normalizehour(hour ?? 0, isPM), 255 | minute: minute ?? 0, 256 | second: second ?? 0, 257 | millisecond: millisecond ?? 0, 258 | offsetMillisec: offsetMillisec ?? 0, 259 | locale: locale.locale, 260 | }; 261 | } 262 | 263 | function normalizehour(hour: number, isPM: boolean) { 264 | if (isPM) { 265 | if (hour < 12) { 266 | return hour + 12; 267 | } 268 | if (hour === 12) { 269 | return 0; 270 | } 271 | } 272 | return hour; 273 | } 274 | 275 | function parseOffsetMillisec(offsetStr: string): number { 276 | if (offsetStr === "Z") return 0; 277 | const parts = offsetStr.match(/([-+]|\d\d)/g); 278 | if (!parts) return 0; 279 | 280 | const hour = parseInteger(parts[1]) ?? 0; 281 | const minute = parseInteger(parts[2]) ?? 0; 282 | const result = minToMillisec(hour * 60 + minute); 283 | if (parts[0] === "-") return result * (-1); 284 | return result; 285 | } 286 | 287 | export function parseISO(isoString: string): ParseResult { 288 | const trimStr = isoString.trim(); 289 | switch (trimStr.length) { 290 | // e.g.: 2021 291 | case 4: 292 | return parseDateStr(trimStr, "YYYY"); 293 | // e.g.: 202107 294 | case 6: 295 | return parseDateStr(trimStr, "YYYYMM"); 296 | // e.g.: 2021-07 or 2021215 (Year and Day of Year) 297 | case 7: 298 | return trimStr.match(/\d{4}-\d{2}/) 299 | ? parseDateStr(trimStr, "YYYY-MM") 300 | : parseDateStr(trimStr, "YYYYDDD"); 301 | case 8: 302 | // e.g.: 20210721 303 | if (trimStr.match(/\d{8}/)) { 304 | return parseDateStr(trimStr, "YYYYMMdd"); 305 | } 306 | // e.g.: 2021215 (Year and Day of Year) 307 | if (trimStr.match(/\d{4}-\d{3}/)) { 308 | return parseDateStr(trimStr, "YYYY-DDD"); 309 | } 310 | // e.g.: 2021W201 (Year and ISO Week Date and Week number) 311 | if (trimStr.match(/\d{4}W\d{3}/)) { 312 | return parseDateStr(trimStr, "YYYY'W'WWw"); 313 | } 314 | return INVALID_DATE; 315 | // e.g.: 2021-07-21 or 2021-W20-1 (Year and ISO Week Date and Week number) 316 | case 10: 317 | if (trimStr.match(/\d{4}W\d{2}-\d/)) { 318 | return parseDateStr(trimStr, "YYYY'W'WWw"); 319 | } 320 | return parseDateStr(trimStr, "YYYY-MM-dd"); 321 | // e.g.: 2021-07-21T13 322 | case 13: 323 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hh"); 324 | // e.g.: 2021-07-21T1325 325 | case 15: 326 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hhmm"); 327 | // e.g.: 2021-07-21T13:25 328 | case 16: 329 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hh:mm"); 330 | // e.g.: 2021-07-21T13:25:30 331 | case 17: 332 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hhmmss"); 333 | // e.g.: 2021-07-21 13:25:30 334 | case 18: 335 | return parseDateStr(trimStr, "YYYY-MM-ddhh:mm:ss"); 336 | // e.g.: 2021-07-21T13:25:30 337 | case 19: 338 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hh:mm:ss"); 339 | // e.g.: 2021-07-21T132530.200 340 | case 21: 341 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hhmmss.S"); 342 | // e.g.: 2021-07-21T13:25:30.200 343 | case 23: 344 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hh:mm:ss.S"); 345 | case 29: 346 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hh:mm:ss.SZZ"); 347 | default: 348 | return parseDateStr(trimStr, "YYYY-MM-dd'T'hh:mm:ss.S"); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /parse_date_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 2 | import { parseDateStr, parseISO } from "./parse_date.ts"; 3 | import { INVALID_DATE } from "./utils.ts"; 4 | 5 | Deno.test("parseDateStr valid", () => { 6 | const tests = [ 7 | { 8 | dateStr: "20210531", 9 | format: "YYYYMMdd", 10 | expected: { 11 | year: 2021, 12 | month: 5, 13 | day: 31, 14 | hour: 0, 15 | minute: 0, 16 | second: 0, 17 | millisecond: 0, 18 | offsetMillisec: 0, 19 | locale: "en", 20 | }, 21 | }, 22 | { 23 | dateStr: "2021-05-31 21:05:30", 24 | format: "YYYY-MM-dd HH:mm:ss", 25 | expected: { 26 | year: 2021, 27 | month: 5, 28 | day: 31, 29 | hour: 21, 30 | minute: 5, 31 | second: 30, 32 | millisecond: 0, 33 | offsetMillisec: 0, 34 | locale: "en", 35 | }, 36 | }, 37 | { 38 | dateStr: "2021-05-31 09:30 AM", 39 | format: "YYYY-MM-dd hh:mm a", 40 | expected: { 41 | year: 2021, 42 | month: 5, 43 | day: 31, 44 | hour: 9, 45 | minute: 30, 46 | second: 0, 47 | millisecond: 0, 48 | offsetMillisec: 0, 49 | locale: "en", 50 | }, 51 | }, 52 | { 53 | dateStr: "2021-05-31 09:30 PM", 54 | format: "YYYY-MM-dd hh:mm a", 55 | expected: { 56 | year: 2021, 57 | month: 5, 58 | day: 31, 59 | hour: 21, 60 | minute: 30, 61 | second: 0, 62 | millisecond: 0, 63 | offsetMillisec: 0, 64 | locale: "en", 65 | }, 66 | }, 67 | { 68 | dateStr: "2021-05-31 09:30Z", 69 | format: "YYYY-MM-dd hh:mmZ", 70 | expected: { 71 | year: 2021, 72 | month: 5, 73 | day: 31, 74 | hour: 9, 75 | minute: 30, 76 | second: 0, 77 | millisecond: 0, 78 | offsetMillisec: 0, 79 | locale: "en", 80 | }, 81 | }, 82 | { 83 | dateStr: "2021-05-31 09:30 +09:00", 84 | format: "YYYY-MM-dd hh:mm Z", 85 | expected: { 86 | year: 2021, 87 | month: 5, 88 | day: 31, 89 | hour: 9, 90 | minute: 30, 91 | second: 0, 92 | millisecond: 0, 93 | offsetMillisec: 32400000, 94 | locale: "en", 95 | }, 96 | }, 97 | { 98 | dateStr: "2021-05-31 09:30 -09:00", 99 | format: "YYYY-MM-dd hh:mm Z", 100 | expected: { 101 | year: 2021, 102 | month: 5, 103 | day: 31, 104 | hour: 9, 105 | minute: 30, 106 | second: 0, 107 | millisecond: 0, 108 | offsetMillisec: -32400000, 109 | locale: "en", 110 | }, 111 | }, 112 | { 113 | dateStr: "2021-1", 114 | format: "YYYY-D", 115 | expected: { 116 | year: 2021, 117 | month: 1, 118 | day: 1, 119 | hour: 0, 120 | minute: 0, 121 | second: 0, 122 | millisecond: 0, 123 | offsetMillisec: 0, 124 | locale: "en", 125 | }, 126 | }, 127 | { 128 | dateStr: "2021-365", 129 | format: "YYYY-D", 130 | expected: { 131 | year: 2021, 132 | month: 12, 133 | day: 31, 134 | hour: 0, 135 | minute: 0, 136 | second: 0, 137 | millisecond: 0, 138 | offsetMillisec: 0, 139 | locale: "en", 140 | }, 141 | }, 142 | { 143 | dateStr: "2021-365", 144 | format: "YYYY-DDD", 145 | expected: { 146 | year: 2021, 147 | month: 12, 148 | day: 31, 149 | hour: 0, 150 | minute: 0, 151 | second: 0, 152 | millisecond: 0, 153 | offsetMillisec: 0, 154 | locale: "en", 155 | }, 156 | }, 157 | { 158 | dateStr: "2021 January", 159 | format: "YYYY MMMM", 160 | expected: { 161 | year: 2021, 162 | month: 1, 163 | day: 0, 164 | hour: 0, 165 | minute: 0, 166 | second: 0, 167 | millisecond: 0, 168 | offsetMillisec: 0, 169 | locale: "en", 170 | }, 171 | }, 172 | { 173 | dateStr: "2021 Jan", 174 | format: "YYYY MMM", 175 | expected: { 176 | year: 2021, 177 | month: 1, 178 | day: 0, 179 | hour: 0, 180 | minute: 0, 181 | second: 0, 182 | millisecond: 0, 183 | offsetMillisec: 0, 184 | locale: "en", 185 | }, 186 | }, 187 | { 188 | dateStr: "2021-06-20 01:02:03.004 AM +01:00", 189 | format: "YYYY-MM-dd HH:mm:ss.S a Z", 190 | expected: { 191 | year: 2021, 192 | month: 6, 193 | day: 20, 194 | hour: 1, 195 | minute: 2, 196 | second: 3, 197 | millisecond: 4, 198 | offsetMillisec: 3600000, 199 | locale: "en", 200 | }, 201 | }, 202 | { 203 | dateStr: "5/Aug/2021:14:15:30 +0900", 204 | format: "d/MMM/YYYY:HH:mm:ss ZZ", 205 | expected: { 206 | year: 2021, 207 | month: 8, 208 | day: 5, 209 | hour: 14, 210 | minute: 15, 211 | second: 30, 212 | millisecond: 0, 213 | offsetMillisec: 32400000, 214 | locale: "en", 215 | }, 216 | }, 217 | { 218 | dateStr: "1.1.2021 1:2:3:4 PM -0100", 219 | format: "d/M/YYYY H:m:s:S a ZZ", 220 | expected: { 221 | year: 2021, 222 | month: 1, 223 | day: 1, 224 | hour: 13, 225 | minute: 2, 226 | second: 3, 227 | millisecond: 4, 228 | offsetMillisec: -3600000, 229 | locale: "en", 230 | }, 231 | }, 232 | { 233 | dateStr: "23_Jan_2021_141523", 234 | format: "dd_MMM_YYYY_hhmmss", 235 | expected: { 236 | year: 2021, 237 | month: 1, 238 | day: 23, 239 | hour: 14, 240 | minute: 15, 241 | second: 23, 242 | millisecond: 0, 243 | offsetMillisec: 0, 244 | locale: "en", 245 | }, 246 | }, 247 | { 248 | dateStr: "20210112", 249 | format: "YYYYMMdd", 250 | expected: { 251 | year: 2021, 252 | month: 1, 253 | day: 12, 254 | hour: 0, 255 | minute: 0, 256 | second: 0, 257 | millisecond: 0, 258 | offsetMillisec: 0, 259 | locale: "en", 260 | }, 261 | }, 262 | ]; 263 | tests.forEach((t) => { 264 | assertEquals(parseDateStr(t.dateStr, t.format), t.expected); 265 | }); 266 | }); 267 | 268 | Deno.test("parseDateStr invalid", () => { 269 | const tests = [ 270 | { 271 | dateStr: "21-04-2021", 272 | format: "YYYY-MM-dd", 273 | }, 274 | { 275 | dateStr: "21/04/2021", 276 | format: "YYYY-MM-dd", 277 | }, 278 | { 279 | dateStr: "2021 Jan", 280 | format: "YY DDD", 281 | }, 282 | { 283 | dateStr: "2021 Turnip 03", 284 | format: "YYYY MMMM DD", 285 | }, 286 | { 287 | dateStr: "2021-18-03-01", 288 | format: "YYYY-MM-dd", 289 | }, 290 | { 291 | dateStr: "2021-01-32", 292 | format: "YYYY-MM-dd", 293 | }, 294 | ]; 295 | tests.forEach((t) => { 296 | assertEquals(parseDateStr(t.dateStr, t.format), INVALID_DATE); 297 | }); 298 | }); 299 | 300 | Deno.test("parseDateStr locale", () => { 301 | const tests = [ 302 | { 303 | dateStr: "2021 1月", 304 | format: "YYYY MMM", 305 | locale: "ja", 306 | expected: { 307 | year: 2021, 308 | month: 1, 309 | day: 0, 310 | hour: 0, 311 | minute: 0, 312 | second: 0, 313 | millisecond: 0, 314 | offsetMillisec: 0, 315 | locale: "ja", 316 | }, 317 | }, 318 | { 319 | dateStr: "2021 лютий 03", 320 | format: "YYYY MMMM dd", 321 | locale: "uk", 322 | expected: { 323 | year: 2021, 324 | month: 2, 325 | day: 3, 326 | hour: 0, 327 | minute: 0, 328 | second: 0, 329 | millisecond: 0, 330 | offsetMillisec: 0, 331 | locale: "uk", 332 | }, 333 | }, 334 | ]; 335 | tests.forEach((t) => { 336 | assertEquals( 337 | parseDateStr(t.dateStr, t.format, { locale: t.locale }), 338 | t.expected, 339 | ); 340 | }); 341 | }); 342 | 343 | Deno.test("parseISO", () => { 344 | const tests = [ 345 | { 346 | dateStr: "2021", 347 | expected: { 348 | year: 2021, 349 | month: 0, 350 | day: 0, 351 | hour: 0, 352 | minute: 0, 353 | second: 0, 354 | millisecond: 0, 355 | offsetMillisec: 0, 356 | locale: "en", 357 | }, 358 | }, 359 | { 360 | dateStr: "202106", 361 | expected: { 362 | year: 2021, 363 | month: 6, 364 | day: 0, 365 | hour: 0, 366 | minute: 0, 367 | second: 0, 368 | millisecond: 0, 369 | offsetMillisec: 0, 370 | locale: "en", 371 | }, 372 | }, 373 | { 374 | dateStr: "2021-06", 375 | expected: { 376 | year: 2021, 377 | month: 6, 378 | day: 0, 379 | hour: 0, 380 | minute: 0, 381 | second: 0, 382 | millisecond: 0, 383 | offsetMillisec: 0, 384 | locale: "en", 385 | }, 386 | }, 387 | { 388 | dateStr: "2021-365", 389 | expected: { 390 | year: 2021, 391 | month: 12, 392 | day: 31, 393 | hour: 0, 394 | minute: 0, 395 | second: 0, 396 | millisecond: 0, 397 | offsetMillisec: 0, 398 | locale: "en", 399 | }, 400 | }, 401 | { 402 | dateStr: "20210430", 403 | expected: { 404 | year: 2021, 405 | month: 4, 406 | day: 30, 407 | hour: 0, 408 | minute: 0, 409 | second: 0, 410 | millisecond: 0, 411 | offsetMillisec: 0, 412 | locale: "en", 413 | }, 414 | }, 415 | { 416 | dateStr: "2021-001", 417 | expected: { 418 | year: 2021, 419 | month: 1, 420 | day: 1, 421 | hour: 0, 422 | minute: 0, 423 | second: 0, 424 | millisecond: 0, 425 | offsetMillisec: 0, 426 | locale: "en", 427 | }, 428 | }, 429 | // length 10 430 | // { 431 | // dateStr: "2021-W25-6", 432 | // expected: { 433 | // year: 2021, 434 | // month: 6, 435 | // day: 26, 436 | // hour: 0, 437 | // minute: 0, 438 | // second: 0, 439 | // millisecond: 0, 440 | // offsetMillisec: 0, 441 | // locale: 'en', 442 | // }, 443 | // }, 444 | // length 12 445 | { 446 | dateStr: "2021-06-30T21", 447 | expected: { 448 | year: 2021, 449 | month: 6, 450 | day: 30, 451 | hour: 21, 452 | minute: 0, 453 | second: 0, 454 | millisecond: 0, 455 | offsetMillisec: 0, 456 | locale: "en", 457 | }, 458 | }, 459 | // length 15 460 | { 461 | dateStr: "2021-06-30T2115", 462 | expected: { 463 | year: 2021, 464 | month: 6, 465 | day: 30, 466 | hour: 21, 467 | minute: 15, 468 | second: 0, 469 | millisecond: 0, 470 | offsetMillisec: 0, 471 | locale: "en", 472 | }, 473 | }, 474 | // length 16 475 | { 476 | dateStr: "2021-06-30T21:15", 477 | expected: { 478 | year: 2021, 479 | month: 6, 480 | day: 30, 481 | hour: 21, 482 | minute: 15, 483 | second: 0, 484 | millisecond: 0, 485 | offsetMillisec: 0, 486 | locale: "en", 487 | }, 488 | }, 489 | // length 18 490 | { 491 | dateStr: "2021-04-15 09:24:15", 492 | expected: { 493 | year: 2021, 494 | month: 4, 495 | day: 15, 496 | hour: 9, 497 | minute: 24, 498 | second: 15, 499 | millisecond: 0, 500 | offsetMillisec: 0, 501 | locale: "en", 502 | }, 503 | }, 504 | // length 19 505 | { 506 | dateStr: "2021-06-30T21:15:30", 507 | expected: { 508 | year: 2021, 509 | month: 6, 510 | day: 30, 511 | hour: 21, 512 | minute: 15, 513 | second: 30, 514 | millisecond: 0, 515 | offsetMillisec: 0, 516 | locale: "en", 517 | }, 518 | }, 519 | // length 21 520 | { 521 | dateStr: "2021-06-30T211530.200", 522 | expected: { 523 | year: 2021, 524 | month: 6, 525 | day: 30, 526 | hour: 21, 527 | minute: 15, 528 | second: 30, 529 | millisecond: 200, 530 | offsetMillisec: 0, 531 | locale: "en", 532 | }, 533 | }, 534 | // length 23 535 | { 536 | dateStr: "2021-06-30T21:15:30.200", 537 | expected: { 538 | year: 2021, 539 | month: 6, 540 | day: 30, 541 | hour: 21, 542 | minute: 15, 543 | second: 30, 544 | millisecond: 200, 545 | offsetMillisec: 0, 546 | locale: "en", 547 | }, 548 | }, 549 | { 550 | dateStr: "2021-06-30T21:15:30.200Z", 551 | expected: { 552 | year: 2021, 553 | month: 6, 554 | day: 30, 555 | hour: 21, 556 | minute: 15, 557 | second: 30, 558 | millisecond: 200, 559 | offsetMillisec: 0, 560 | locale: "en", 561 | }, 562 | }, 563 | { 564 | dateStr: "2021-06-30T21:15:30.200+09:00", 565 | expected: { 566 | year: 2021, 567 | month: 6, 568 | day: 30, 569 | hour: 21, 570 | minute: 15, 571 | second: 30, 572 | millisecond: 200, 573 | offsetMillisec: 32400000, 574 | locale: "en", 575 | }, 576 | }, 577 | { 578 | dateStr: "2021-06-30T21:15:30.200-09:00", 579 | expected: { 580 | year: 2021, 581 | month: 6, 582 | day: 30, 583 | hour: 21, 584 | minute: 15, 585 | second: 30, 586 | millisecond: 200, 587 | offsetMillisec: -32400000, 588 | locale: "en", 589 | }, 590 | }, 591 | ]; 592 | tests.forEach((t) => { 593 | assertEquals( 594 | parseISO(t.dateStr), 595 | t.expected, 596 | ); 597 | }); 598 | }); 599 | -------------------------------------------------------------------------------- /timezone.ts: -------------------------------------------------------------------------------- 1 | import { Timezone } from "./types.ts"; 2 | 3 | type TokenizeDate = { 4 | year: number; 5 | month: number; 6 | day: number; 7 | hour: number; 8 | minute: number; 9 | second: number; 10 | }; 11 | 12 | export function tzOffset(date: Date, tz: Timezone): number { 13 | const tzDate = tzTokenizeDate(date, tz); 14 | 15 | const { year, month, day, hour, minute, second } = tzDate; 16 | const utc = Date.UTC(year, month - 1, day, hour, minute, second); 17 | 18 | let asTS = date.getTime(); 19 | const over = asTS % 1000; 20 | asTS -= over >= 0 ? over : 1000 + over; 21 | return utc - asTS; 22 | } 23 | 24 | function tzTokenizeDate(date: Date, tz: Timezone) { 25 | const dtf = getDateTimeFormat(tz); 26 | return partsOffset(dtf, date); 27 | } 28 | 29 | function partsOffset(dtf: Intl.DateTimeFormat, date: Date): TokenizeDate { 30 | const formatted = dtf.formatToParts(date); 31 | 32 | const hash: { [key: string]: number } = {}; 33 | 34 | for (const f of formatted) { 35 | hash[f.type] = parseInt(f.value, 10); 36 | } 37 | 38 | return { 39 | year: hash["year"], 40 | month: hash["month"], 41 | day: hash["day"], 42 | hour: hash["hour"], 43 | minute: hash["minute"], 44 | second: hash["second"], 45 | }; 46 | } 47 | 48 | const cache: Map = new Map(); 49 | function getDateTimeFormat(tz: Timezone): Intl.DateTimeFormat { 50 | if (!cache.get(tz)) { 51 | const tmpDateFormat = new Intl.DateTimeFormat("en-US", { 52 | hour12: false, 53 | timeZone: "America/New_York", 54 | year: "numeric", 55 | day: "2-digit", 56 | hour: "2-digit", 57 | minute: "2-digit", 58 | second: "2-digit", 59 | }).format(new Date("2021-05-15T04:00:00.123Z")); 60 | 61 | const isHourCycleSupported = tmpDateFormat === 62 | "05/15/2021, 00:00:00" || 63 | tmpDateFormat === "‎05‎/15‎/‎2021‎ ‎00‎:‎00‎:‎00"; 64 | const format = isHourCycleSupported 65 | ? new Intl.DateTimeFormat("en-US", { 66 | hour12: false, 67 | timeZone: tz, 68 | year: "numeric", 69 | month: "2-digit", 70 | day: "2-digit", 71 | hour: "2-digit", 72 | minute: "2-digit", 73 | second: "2-digit", 74 | }) 75 | : new Intl.DateTimeFormat("en-US", { 76 | hourCycle: "h23", 77 | timeZone: tz, 78 | year: "numeric", 79 | month: "2-digit", 80 | day: "2-digit", 81 | hour: "2-digit", 82 | minute: "2-digit", 83 | second: "2-digit", 84 | }); 85 | cache.set(tz, format); 86 | } 87 | 88 | return cache.get(tz) as Intl.DateTimeFormat; 89 | } 90 | -------------------------------------------------------------------------------- /timezone_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 2 | import { Timezone } from "./types.ts"; 3 | import { tzOffset } from "./timezone.ts"; 4 | import { MILLISECONDS_IN_HOUR } from "./constants.ts"; 5 | 6 | Deno.test("tzOffset", () => { 7 | type Test = { 8 | date: Date; 9 | tz: Timezone; 10 | expected: number; 11 | }; 12 | const tests: Test[] = [ 13 | { date: new Date("2021-05-13T12:15:30Z"), tz: "Asia/Tokyo", expected: 9 }, 14 | { date: new Date("2021-05-13T12:15:30Z"), tz: "UTC", expected: 0 }, 15 | { 16 | date: new Date("2021-05-13T12:15:30Z"), 17 | tz: "America/New_York", 18 | expected: -4, 19 | }, 20 | { 21 | date: new Date("2021-12-13T12:15:30Z"), 22 | tz: "America/New_York", 23 | expected: -5, 24 | }, 25 | ]; 26 | tests.forEach((t) => { 27 | assertEquals(tzOffset(t.date, t.tz), t.expected * MILLISECONDS_IN_HOUR); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export type DateObj = { 2 | year: number; 3 | month: number; 4 | day: number; 5 | hour: number; 6 | minute: number; 7 | second: number; 8 | millisecond: number; 9 | }; 10 | 11 | export type DateArray = [ 12 | number, 13 | number, 14 | number, 15 | number, 16 | number, 17 | number, 18 | number, 19 | ]; 20 | 21 | export type DateDiff = Partial & { 22 | quarter?: number; 23 | weeks?: number; 24 | }; 25 | 26 | export type Option = { 27 | timezone?: Timezone; 28 | offsetMillisec?: number; 29 | locale?: string; 30 | }; 31 | 32 | export const TIMEZONE = [ 33 | "Etc/GMT+12", 34 | "Etc/GMT+11", 35 | "Pacific/Honolulu", 36 | "America/Anchorage", 37 | "America/Santa_Isabel", 38 | "America/Los_Angeles", 39 | "America/Chihuahua", 40 | "America/Phoenix", 41 | "America/Denver", 42 | "America/Guatemala", 43 | "America/Chicago", 44 | "America/Regina", 45 | "America/Mexico_City", 46 | "America/Bogota", 47 | "America/Indiana/Indianapolis", 48 | "America/New_York", 49 | "America/Caracas", 50 | "America/Halifax", 51 | "America/Asuncion", 52 | "America/La_Paz", 53 | "America/Cuiaba", 54 | "America/Santiago", 55 | "America/St_Johns", 56 | "America/Sao_Paulo", 57 | "America/Godthab", 58 | "America/Cayenne", 59 | "America/Argentina/Buenos_Aires", 60 | "America/Montevideo", 61 | "Etc/GMT+2", 62 | "Atlantic/Cape_Verde", 63 | "Atlantic/Azores", 64 | "Africa/Casablanca", 65 | "Atlantic/Reykjavik", 66 | "Europe/London", 67 | "Etc/GMT", 68 | "Europe/Berlin", 69 | "Europe/Paris", 70 | "Africa/Lagos", 71 | "Europe/Budapest", 72 | "Europe/Warsaw", 73 | "Africa/Windhoek", 74 | "Europe/Istanbul", 75 | "Europe/Kiev", 76 | "Africa/Cairo", 77 | "Asia/Damascus", 78 | "Asia/Amman", 79 | "Africa/Johannesburg", 80 | "Asia/Jerusalem", 81 | "Asia/Beirut", 82 | "Asia/Baghdad", 83 | "Europe/Minsk", 84 | "Asia/Riyadh", 85 | "Africa/Nairobi", 86 | "Asia/Tehran", 87 | "Europe/Moscow", 88 | "Asia/Tbilisi", 89 | "Asia/Yerevan", 90 | "Asia/Dubai", 91 | "Asia/Baku", 92 | "Indian/Mauritius", 93 | "Asia/Kabul", 94 | "Asia/Tashkent", 95 | "Asia/Karachi", 96 | "Asia/Colombo", 97 | "Asia/Kolkata", 98 | "Asia/Kathmandu", 99 | "Asia/Almaty", 100 | "Asia/Dhaka", 101 | "Asia/Yekaterinburg", 102 | "Asia/Yangon", 103 | "Asia/Bangkok", 104 | "Asia/Novosibirsk", 105 | "Asia/Krasnoyarsk", 106 | "Asia/Ulaanbaatar", 107 | "Asia/Shanghai", 108 | "Australia/Perth", 109 | "Asia/Singapore", 110 | "Asia/Taipei", 111 | "Asia/Irkutsk", 112 | "Asia/Seoul", 113 | "Asia/Tokyo", 114 | "Australia/Darwin", 115 | "Australia/Adelaide", 116 | "Australia/Hobart", 117 | "Asia/Yakutsk", 118 | "Australia/Brisbane", 119 | "Pacific/Port_Moresby", 120 | "Australia/Sydney", 121 | "Asia/Vladivostok", 122 | "Pacific/Guadalcanal", 123 | "Etc/GMT-12", 124 | "Pacific/Fiji", 125 | "Asia/Magadan", 126 | "Pacific/Auckland", 127 | "Pacific/Tongatapu", 128 | "Pacific/Apia", 129 | "UTC", 130 | ]; 131 | 132 | export type Timezone = typeof TIMEZONE[number]; 133 | 134 | export const DATE_FORMAT = [ 135 | "YY", 136 | "YYYY", 137 | "M", 138 | "MM", 139 | "MMM", 140 | "MMMM", 141 | "d", 142 | "dd", 143 | "D", 144 | "DDD", 145 | "H", 146 | "HH", 147 | "h", 148 | "hh", 149 | "m", 150 | "mm", 151 | "s", 152 | "ss", 153 | "S", 154 | "SSS", 155 | "w", 156 | "www", 157 | "wwww", 158 | "W", 159 | "WW", 160 | "a", 161 | "z", 162 | "Z", 163 | "ZZ", 164 | "ZZZ", 165 | "ZZZZ", 166 | "X", 167 | "x", 168 | ] as const; 169 | 170 | export type DateFormatType = typeof DATE_FORMAT[number]; 171 | 172 | export function isFormatDateType(format: string): format is DateFormatType { 173 | return DATE_FORMAT.includes(format as DateFormatType); 174 | } 175 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import { MILLISECONDS_IN_MINUTE } from "./constants.ts"; 2 | import { DateObj } from "./types.ts"; 3 | 4 | export const INVALID_DATE = { 5 | year: NaN, 6 | month: NaN, 7 | day: NaN, 8 | hour: NaN, 9 | minute: NaN, 10 | second: NaN, 11 | millisecond: NaN, 12 | offsetMillisec: NaN, 13 | } as const; 14 | 15 | export function isLeapYear(year: number): boolean { 16 | return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); 17 | } 18 | 19 | export function daysInYear(year: number): number { 20 | return isLeapYear(year) ? 366 : 365; 21 | } 22 | 23 | export function daysInMonth(year: number, month: number): number { 24 | const modMonth = floorMod(month - 1, 12) + 1, 25 | modYear = year + (month - modMonth) / 12; 26 | if (modMonth === 2) { 27 | return isLeapYear(modYear) ? 29 : 28; 28 | } else { 29 | return [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1]; 30 | } 31 | } 32 | 33 | export function weeksInWeekYear(year: number) { 34 | const p1 = (year + 35 | Math.floor(year / 4) - 36 | Math.floor(year / 100) + 37 | Math.floor(year / 400)) % 38 | 7, 39 | last = year - 1, 40 | p2 = (last + Math.floor(last / 4) - Math.floor(last / 100) + 41 | Math.floor(last / 400)) % 7; 42 | return p1 === 4 || p2 === 3 ? 53 : 52; 43 | } 44 | 45 | export function isBetween(n: number, min: number, max: number): boolean { 46 | return min <= n && n <= max; 47 | } 48 | 49 | function isValidMonth(month: number): boolean { 50 | return isBetween(month, 1, 12); 51 | } 52 | 53 | function isValidDay(day: number, year: number, month: number): boolean { 54 | return isBetween(day, 1, daysInMonth(year, month)); 55 | } 56 | 57 | function isValidHour(hour: number): boolean { 58 | return isBetween(hour, 1, 23); 59 | } 60 | 61 | function isValidMinute(minute: number): boolean { 62 | return isBetween(minute, 0, 59); 63 | } 64 | 65 | function isValidSec(sec: number): boolean { 66 | return isBetween(sec, 0, 59); 67 | } 68 | 69 | function isValidMillisec(millisecond: number): boolean { 70 | return isBetween(millisecond, 0, 999); 71 | } 72 | 73 | export function isValidDate(dateObj: Partial): boolean { 74 | const { year, month, day, hour, minute, second, millisecond } = dateObj; 75 | 76 | if (!year || isNaN(year)) return false; 77 | 78 | if (month && !isValidMonth(month)) return false; 79 | 80 | if (month && day) { 81 | if (!isValidDay(day, year, month)) return false; 82 | } 83 | 84 | if (hour) { 85 | const isValid = isValidHour(hour) || 86 | (hour === 24 && minute === 0 && second === 0 && millisecond === 0); 87 | if (!isValid) return false; 88 | } 89 | 90 | if (minute) { 91 | if (!isValidMinute(minute)) return false; 92 | } 93 | 94 | if (second) { 95 | if (!isValidSec(second)) return false; 96 | } 97 | 98 | if (millisecond) { 99 | if (!isValidMillisec(millisecond)) return false; 100 | } 101 | 102 | return true; 103 | } 104 | 105 | export function isValidOrdinalDate(year: number, ordinal: number): boolean { 106 | const days = daysInYear(year); 107 | 108 | return isBetween(ordinal, 1, days); 109 | } 110 | 111 | export function parseInteger(value: string | undefined): number | undefined { 112 | if (!value) return undefined; 113 | 114 | const parsed = parseInt(value, 10); 115 | if (isNaN(parsed)) return undefined; 116 | 117 | return parsed; 118 | } 119 | 120 | export function formatToTwoDigits(n: number): string { 121 | return n <= 9 ? `0${n}` : n.toString(); 122 | } 123 | 124 | export function formatToThreeDigits(n: number): string { 125 | if (n <= 9) return `00${n}`; 126 | if (n <= 99) return `0${n}`; 127 | return n.toString(); 128 | } 129 | 130 | export function weeksOfYear(year: number): number { 131 | const p1 = (year + 132 | Math.floor(year / 4) - 133 | Math.floor(year / 100) + 134 | Math.floor(year / 400)) % 135 | 7, 136 | last = year - 1, 137 | p2 = (last + Math.floor(last / 4) - Math.floor(last / 100) + 138 | Math.floor(last / 400)) % 7; 139 | return p1 === 4 || p2 === 3 ? 53 : 52; 140 | } 141 | 142 | export function truncNumber(n?: number): number { 143 | if (!n || isNaN(n)) { 144 | return 0; 145 | } 146 | return Math.trunc(n); 147 | } 148 | 149 | export function floorMod(x: number, n: number) { 150 | return x - n * Math.floor(x / n); 151 | } 152 | 153 | export function millisecToMin(millisec: number): number { 154 | return millisec / MILLISECONDS_IN_MINUTE; 155 | } 156 | 157 | export function minToMillisec(min: number) { 158 | return min * MILLISECONDS_IN_MINUTE; 159 | } 160 | -------------------------------------------------------------------------------- /zoned_time.ts: -------------------------------------------------------------------------------- 1 | import { tzOffset } from "./timezone.ts"; 2 | import { DateObj, Timezone } from "./types.ts"; 3 | import { dateToJSDate, dateToTS, jsDateToDate } from "./convert.ts"; 4 | 5 | export function utcToZonedTime(date: DateObj, tz: Timezone): DateObj { 6 | const offset = tzOffset(dateToJSDate(date), tz); 7 | const d = new Date(dateToTS(date) + offset); 8 | return jsDateToDate(d); 9 | } 10 | 11 | export function zonedTimeToUTC(date: DateObj, tz: Timezone): DateObj { 12 | const offset = tzOffset(dateToJSDate(date), tz); 13 | const d = new Date(dateToTS(date) - offset); 14 | return jsDateToDate(d); 15 | } 16 | 17 | export function diffOffset( 18 | date: DateObj, 19 | baseTZ: Timezone, 20 | compareTZ: Timezone, 21 | ): number { 22 | if (baseTZ === compareTZ) return 0; 23 | const baseOffset = tzOffset(dateToJSDate(date), baseTZ); 24 | const compareOffset = tzOffset(dateToJSDate(date), compareTZ); 25 | return baseOffset - compareOffset; 26 | } 27 | 28 | export function toOtherZonedTime( 29 | date: DateObj, 30 | baseTZ: Timezone, 31 | compareTZ: Timezone, 32 | ): DateObj { 33 | const d = new Date(dateToTS(date) - diffOffset(date, baseTZ, compareTZ)); 34 | return jsDateToDate(d); 35 | } 36 | -------------------------------------------------------------------------------- /zoned_time_test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | diffOffset, 3 | toOtherZonedTime, 4 | utcToZonedTime, 5 | zonedTimeToUTC, 6 | } from "./zoned_time.ts"; 7 | import { assertEquals } from "https://deno.land/std@0.95.0/testing/asserts.ts"; 8 | import { DateObj, Timezone } from "./types.ts"; 9 | import { MILLISECONDS_IN_HOUR } from "./constants.ts"; 10 | 11 | Deno.test("utcToZonedTime", () => { 12 | type Test = { 13 | date: DateObj; 14 | tz: Timezone; 15 | expected: DateObj; 16 | }; 17 | const tests: Test[] = [ 18 | { 19 | date: { 20 | year: 2021, 21 | month: 5, 22 | day: 13, 23 | hour: 12, 24 | minute: 15, 25 | second: 30, 26 | millisecond: 0, 27 | }, 28 | tz: "UTC", 29 | expected: { 30 | year: 2021, 31 | month: 5, 32 | day: 13, 33 | hour: 12, 34 | minute: 15, 35 | second: 30, 36 | millisecond: 0, 37 | }, 38 | }, 39 | { 40 | date: { 41 | year: 2021, 42 | month: 5, 43 | day: 13, 44 | hour: 12, 45 | minute: 15, 46 | second: 30, 47 | millisecond: 0, 48 | }, 49 | tz: "Asia/Tokyo", 50 | expected: { 51 | year: 2021, 52 | month: 5, 53 | day: 13, 54 | hour: 21, 55 | minute: 15, 56 | second: 30, 57 | millisecond: 0, 58 | }, 59 | }, 60 | { 61 | date: { 62 | year: 2021, 63 | month: 5, 64 | day: 13, 65 | hour: 12, 66 | minute: 15, 67 | second: 30, 68 | millisecond: 0, 69 | }, 70 | tz: "America/New_York", 71 | expected: { 72 | year: 2021, 73 | month: 5, 74 | day: 13, 75 | hour: 8, 76 | minute: 15, 77 | second: 30, 78 | millisecond: 0, 79 | }, 80 | }, 81 | { 82 | date: { 83 | year: 2021, 84 | month: 11, 85 | day: 13, 86 | hour: 12, 87 | minute: 15, 88 | second: 30, 89 | millisecond: 0, 90 | }, 91 | tz: "America/New_York", 92 | expected: { 93 | year: 2021, 94 | month: 11, 95 | day: 13, 96 | hour: 7, 97 | minute: 15, 98 | second: 30, 99 | millisecond: 0, 100 | }, 101 | }, 102 | ]; 103 | 104 | tests.forEach((t) => { 105 | assertEquals(utcToZonedTime(t.date, t.tz), t.expected); 106 | }); 107 | }); 108 | 109 | Deno.test("zonedTimeToUTC", () => { 110 | type Test = { 111 | date: DateObj; 112 | tz: Timezone; 113 | expected: DateObj; 114 | }; 115 | 116 | const tests: Test[] = [ 117 | { 118 | date: { 119 | year: 2021, 120 | month: 5, 121 | day: 13, 122 | hour: 12, 123 | minute: 15, 124 | second: 30, 125 | millisecond: 0, 126 | }, 127 | tz: "UTC", 128 | expected: { 129 | year: 2021, 130 | month: 5, 131 | day: 13, 132 | hour: 12, 133 | minute: 15, 134 | second: 30, 135 | millisecond: 0, 136 | }, 137 | }, 138 | { 139 | date: { 140 | year: 2021, 141 | month: 5, 142 | day: 13, 143 | hour: 21, 144 | minute: 15, 145 | second: 30, 146 | millisecond: 0, 147 | }, 148 | tz: "Asia/Tokyo", 149 | expected: { 150 | year: 2021, 151 | month: 5, 152 | day: 13, 153 | hour: 12, 154 | minute: 15, 155 | second: 30, 156 | millisecond: 0, 157 | }, 158 | }, 159 | { 160 | date: { 161 | year: 2021, 162 | month: 5, 163 | day: 13, 164 | hour: 8, 165 | minute: 15, 166 | second: 30, 167 | millisecond: 0, 168 | }, 169 | tz: "America/New_York", 170 | expected: { 171 | year: 2021, 172 | month: 5, 173 | day: 13, 174 | hour: 12, 175 | minute: 15, 176 | second: 30, 177 | millisecond: 0, 178 | }, 179 | }, 180 | { 181 | date: { 182 | year: 2021, 183 | month: 11, 184 | day: 13, 185 | hour: 7, 186 | minute: 15, 187 | second: 30, 188 | millisecond: 0, 189 | }, 190 | tz: "America/New_York", 191 | expected: { 192 | year: 2021, 193 | month: 11, 194 | day: 13, 195 | hour: 12, 196 | minute: 15, 197 | second: 30, 198 | millisecond: 0, 199 | }, 200 | }, 201 | ]; 202 | 203 | tests.forEach((t) => { 204 | assertEquals(zonedTimeToUTC(t.date, t.tz), t.expected); 205 | }); 206 | }); 207 | 208 | Deno.test("diffOffset", () => { 209 | type Test = { 210 | date: DateObj; 211 | baseTZ: Timezone; 212 | compareTZ: Timezone; 213 | expected: number; 214 | }; 215 | 216 | const tests: Test[] = [ 217 | { 218 | date: { 219 | year: 2021, 220 | month: 5, 221 | day: 13, 222 | hour: 12, 223 | minute: 15, 224 | second: 30, 225 | millisecond: 0, 226 | }, 227 | baseTZ: "UTC", 228 | compareTZ: "Asia/Tokyo", 229 | expected: -9, 230 | }, 231 | { 232 | date: { 233 | year: 2021, 234 | month: 5, 235 | day: 13, 236 | hour: 12, 237 | minute: 15, 238 | second: 30, 239 | millisecond: 0, 240 | }, 241 | baseTZ: "Asia/Tokyo", 242 | compareTZ: "America/New_York", 243 | expected: 13, 244 | }, 245 | ]; 246 | 247 | tests.forEach((t) => { 248 | assertEquals( 249 | diffOffset(t.date, t.baseTZ, t.compareTZ) / MILLISECONDS_IN_HOUR, 250 | t.expected, 251 | ); 252 | }); 253 | }); 254 | 255 | Deno.test("toOtherZonedTime", () => { 256 | type Test = { 257 | date: DateObj; 258 | baseTZ: Timezone; 259 | compareTZ: Timezone; 260 | expected: DateObj; 261 | }; 262 | 263 | const tests: Test[] = [ 264 | { 265 | date: { 266 | year: 2021, 267 | month: 5, 268 | day: 13, 269 | hour: 12, 270 | minute: 15, 271 | second: 30, 272 | millisecond: 0, 273 | }, 274 | baseTZ: "UTC", 275 | compareTZ: "Asia/Tokyo", 276 | expected: { 277 | year: 2021, 278 | month: 5, 279 | day: 13, 280 | hour: 21, 281 | minute: 15, 282 | second: 30, 283 | millisecond: 0, 284 | }, 285 | }, 286 | { 287 | date: { 288 | year: 2021, 289 | month: 5, 290 | day: 13, 291 | hour: 12, 292 | minute: 15, 293 | second: 30, 294 | millisecond: 0, 295 | }, 296 | baseTZ: "Asia/Tokyo", 297 | compareTZ: "America/New_York", 298 | expected: { 299 | year: 2021, 300 | month: 5, 301 | day: 12, 302 | hour: 23, 303 | minute: 15, 304 | second: 30, 305 | millisecond: 0, 306 | }, 307 | }, 308 | ]; 309 | 310 | tests.forEach((t) => { 311 | assertEquals( 312 | toOtherZonedTime(t.date, t.baseTZ, t.compareTZ), 313 | t.expected, 314 | ); 315 | }); 316 | }); 317 | --------------------------------------------------------------------------------