mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	time_zone_util: Add zoned date/time utility functions.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							ff9c15ea83
						
					
				
				
					commit
					33e335dbd1
				
			
							
								
								
									
										54
									
								
								web/src/time_zone_util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								web/src/time_zone_util.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
import assert from "minimalistic-assert";
 | 
			
		||||
 | 
			
		||||
const offset_formats = new Map<string, Intl.DateTimeFormat>();
 | 
			
		||||
 | 
			
		||||
function get_offset_format(time_zone: string): Intl.DateTimeFormat {
 | 
			
		||||
    let format = offset_formats.get(time_zone);
 | 
			
		||||
    if (format === undefined) {
 | 
			
		||||
        format = new Intl.DateTimeFormat("en-US", {
 | 
			
		||||
            timeZoneName: "longOffset",
 | 
			
		||||
            timeZone: time_zone,
 | 
			
		||||
        });
 | 
			
		||||
        offset_formats.set(time_zone, format);
 | 
			
		||||
    }
 | 
			
		||||
    return format;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Get the given time zone's offset in milliseconds at the given date. */
 | 
			
		||||
export function get_offset(date: number | Date, time_zone: string): number {
 | 
			
		||||
    const offset_string = get_offset_format(time_zone)
 | 
			
		||||
        .formatToParts(date)
 | 
			
		||||
        .find((part) => part.type === "timeZoneName")!.value;
 | 
			
		||||
    if (offset_string === "GMT") {
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
    const m = /^GMT(([+-])\d\d):(\d\d)$/.exec(offset_string);
 | 
			
		||||
    assert(m !== null, offset_string);
 | 
			
		||||
    return (Number(m[1]) * 60 + Number(m[2] + m[3])) * 60000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Get the start of the day for the given date in the given time zone. */
 | 
			
		||||
export function start_of_day(date: number | Date, time_zone: string): Date {
 | 
			
		||||
    const offset = get_offset(date, time_zone);
 | 
			
		||||
    let t = Number(date) + offset;
 | 
			
		||||
    t -= t % 86400000;
 | 
			
		||||
    return new Date(t - get_offset(new Date(t - offset), time_zone));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Get the number of calendar days between the given dates (ignoring times) in
 | 
			
		||||
 * the given time zone. */
 | 
			
		||||
export function difference_in_calendar_days(
 | 
			
		||||
    left: number | Date,
 | 
			
		||||
    right: number | Date,
 | 
			
		||||
    time_zone: string,
 | 
			
		||||
): number {
 | 
			
		||||
    return Math.round(
 | 
			
		||||
        (start_of_day(left, time_zone).getTime() - start_of_day(right, time_zone).getTime()) /
 | 
			
		||||
            86400000,
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Are the given dates in the same day in the given time zone? */
 | 
			
		||||
export function is_same_day(left: number | Date, right: number | Date, time_zone: string): boolean {
 | 
			
		||||
    return start_of_day(left, time_zone).getTime() === start_of_day(right, time_zone).getTime();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								web/tests/time_zone_util.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								web/tests/time_zone_util.test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const {strict: assert} = require("assert");
 | 
			
		||||
 | 
			
		||||
const {zrequire} = require("./lib/namespace");
 | 
			
		||||
const {run_test} = require("./lib/test");
 | 
			
		||||
 | 
			
		||||
const {get_offset, start_of_day, is_same_day, difference_in_calendar_days} =
 | 
			
		||||
    zrequire("time_zone_util");
 | 
			
		||||
 | 
			
		||||
function pre(date) {
 | 
			
		||||
    return new Date(date.getTime() - 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ny = "America/New_York";
 | 
			
		||||
const ny_new_year = new Date("2023-01-01T05:00Z");
 | 
			
		||||
const ny_new_year_eve = new Date("2022-12-31T05:00Z");
 | 
			
		||||
const st_johns = "America/St_Johns";
 | 
			
		||||
const st_johns_dst_begin = new Date("2023-03-12T05:30Z");
 | 
			
		||||
const st_johns_dst_end = new Date("2023-11-05T04:30Z");
 | 
			
		||||
const chatham = "Pacific/Chatham";
 | 
			
		||||
const chatham_dst_begin = new Date("2022-09-24T14:00Z");
 | 
			
		||||
const chatham_dst_end = new Date("2023-04-01T14:00Z");
 | 
			
		||||
const kiritimati = "Pacific/Kiritimati";
 | 
			
		||||
const kiritimati_date_skip = new Date("1994-12-31T10:00Z");
 | 
			
		||||
 | 
			
		||||
run_test("get_offset", () => {
 | 
			
		||||
    assert.equal(get_offset(ny_new_year, "UTC"), 0);
 | 
			
		||||
    assert.equal(get_offset(ny_new_year, ny), -5 * 60 * 60000);
 | 
			
		||||
    assert.equal(get_offset(pre(st_johns_dst_begin), st_johns), -(3 * 60 + 30) * 60000);
 | 
			
		||||
    assert.equal(get_offset(st_johns_dst_begin, st_johns), -(2 * 60 + 30) * 60000);
 | 
			
		||||
    assert.equal(get_offset(pre(st_johns_dst_end), st_johns), -(2 * 60 + 30) * 60000);
 | 
			
		||||
    assert.equal(get_offset(st_johns_dst_end, st_johns), -(3 * 60 + 30) * 60000);
 | 
			
		||||
    assert.equal(get_offset(pre(chatham_dst_begin), chatham), (12 * 60 + 45) * 60000);
 | 
			
		||||
    assert.equal(get_offset(chatham_dst_begin, chatham), (13 * 60 + 45) * 60000);
 | 
			
		||||
    assert.equal(get_offset(pre(chatham_dst_end), chatham), (13 * 60 + 45) * 60000);
 | 
			
		||||
    assert.equal(get_offset(chatham_dst_end, chatham), (12 * 60 + 45) * 60000);
 | 
			
		||||
    assert.equal(get_offset(pre(kiritimati_date_skip), kiritimati), -10 * 60 * 60000);
 | 
			
		||||
    assert.equal(get_offset(kiritimati_date_skip, kiritimati), 14 * 60 * 60000);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("start_of_day", () => {
 | 
			
		||||
    for (const [date, time_zone] of [
 | 
			
		||||
        [pre(ny_new_year), "UTC"],
 | 
			
		||||
        [ny_new_year, "UTC"],
 | 
			
		||||
        [pre(ny_new_year), ny],
 | 
			
		||||
        [ny_new_year, ny],
 | 
			
		||||
        [pre(st_johns_dst_begin), st_johns],
 | 
			
		||||
        [st_johns_dst_end, st_johns],
 | 
			
		||||
        [pre(st_johns_dst_end), st_johns],
 | 
			
		||||
        [st_johns_dst_end, st_johns],
 | 
			
		||||
        [pre(chatham_dst_begin), chatham],
 | 
			
		||||
        [chatham_dst_end, chatham],
 | 
			
		||||
        [pre(chatham_dst_end), chatham],
 | 
			
		||||
        [chatham_dst_end, chatham],
 | 
			
		||||
        [pre(kiritimati_date_skip), kiritimati],
 | 
			
		||||
        [kiritimati_date_skip, kiritimati],
 | 
			
		||||
    ]) {
 | 
			
		||||
        const start = start_of_day(date, time_zone);
 | 
			
		||||
        assert.equal(
 | 
			
		||||
            start.toLocaleDateString("en-US", {timeZone: time_zone}),
 | 
			
		||||
            date.toLocaleDateString("en-US", {timeZone: time_zone}),
 | 
			
		||||
        );
 | 
			
		||||
        assert.equal(start.toLocaleTimeString("en-US", {timeZone: time_zone}), "12:00:00 AM");
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("is_same_day", () => {
 | 
			
		||||
    assert.ok(is_same_day(pre(ny_new_year), ny_new_year_eve, ny));
 | 
			
		||||
    assert.ok(!is_same_day(pre(ny_new_year), ny_new_year, ny));
 | 
			
		||||
    assert.ok(is_same_day(pre(st_johns_dst_begin), st_johns_dst_begin, st_johns));
 | 
			
		||||
    assert.ok(is_same_day(pre(st_johns_dst_end), st_johns_dst_end, st_johns));
 | 
			
		||||
    assert.ok(is_same_day(pre(chatham_dst_begin), chatham_dst_begin, chatham));
 | 
			
		||||
    assert.ok(is_same_day(pre(chatham_dst_end), chatham_dst_end, chatham));
 | 
			
		||||
    assert.ok(!is_same_day(pre(kiritimati_date_skip), kiritimati_date_skip, kiritimati));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("difference_in_calendar_days", () => {
 | 
			
		||||
    assert.equal(difference_in_calendar_days(pre(ny_new_year), ny_new_year, ny), -1);
 | 
			
		||||
    assert.equal(difference_in_calendar_days(pre(ny_new_year), ny_new_year_eve, ny), 0);
 | 
			
		||||
    assert.equal(difference_in_calendar_days(ny_new_year, ny_new_year_eve, ny), 1);
 | 
			
		||||
    assert.equal(difference_in_calendar_days(ny_new_year, pre(ny_new_year_eve), ny), 2);
 | 
			
		||||
    assert.equal(difference_in_calendar_days(st_johns_dst_end, st_johns_dst_begin, st_johns), 238);
 | 
			
		||||
    assert.equal(difference_in_calendar_days(chatham_dst_begin, chatham_dst_end, chatham), -189);
 | 
			
		||||
 | 
			
		||||
    // date-fns gives 2, but 1 seems more correct
 | 
			
		||||
    assert.equal(
 | 
			
		||||
        difference_in_calendar_days(kiritimati_date_skip, pre(kiritimati_date_skip), kiritimati),
 | 
			
		||||
        1,
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user