| // Copyright (C) 2018 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| import {BigintMath} from './bigint_math'; | |
| import type {Brand} from './brand'; | |
| import {assertTrue} from './assert'; | |
| // The |time| type represents trace time in the same units and domain as trace | |
| // processor (i.e. typically boot time in nanoseconds, but most of the UI should | |
| // be completely agnostic to this). | |
| export type time = Brand<bigint, 'time'>; | |
| // The |duration| type is used to represent the duration of time between two | |
| // |time|s. The domain is irrelevant because a duration is relative. | |
| export type duration = bigint; | |
| // The conversion factor for converting between different time units. | |
| const TIME_UNITS_PER_SEC = 1e9; | |
| const TIME_UNITS_PER_MILLISEC = 1e6; | |
| const TIME_UNITS_PER_MICROSEC = 1e3; | |
| export class Time { | |
| // Negative time is never found in a trace - so -1 is commonly used as a flag | |
| // to represent a value is undefined or unset, without having to use a | |
| // nullable or union type. | |
| static readonly INVALID = Time.fromRaw(-1n); | |
| // The min and max possible values, considering times cannot be negative. | |
| static readonly MIN = Time.fromRaw(0n); | |
| static readonly MAX = Time.fromRaw(BigintMath.INT64_MAX); | |
| static readonly ZERO = Time.fromRaw(0n); | |
| // Cast a bigint to a |time|. Supports potentially |undefined| values. | |
| // I.e. it performs the following conversions: | |
| // - `bigint` -> `time` | |
| // - `bigint|undefined` -> `time|undefined` | |
| // | |
| // Use this function with caution. The function is effectively a no-op in JS, | |
| // but using it tells TypeScript that "this value is a time value". It's up to | |
| // the caller to ensure the value is in the correct units and time domain. | |
| // | |
| // If you're reaching for this function after doing some maths on a |time| | |
| // value and it's decayed to a |bigint| consider using the static math methods | |
| // in |Time| instead, as they will do the appropriate casting for you. | |
| static fromRaw(v: bigint): time; | |
| static fromRaw(v?: bigint): time | undefined; | |
| static fromRaw(v?: bigint): time | undefined { | |
| return v as time | undefined; | |
| } | |
| // Convert seconds (number) to a time value. | |
| // Note: number -> BigInt conversion is relatively slow. | |
| static fromSeconds(seconds: number): time { | |
| return Time.fromRaw(BigInt(Math.floor(seconds * TIME_UNITS_PER_SEC))); | |
| } | |
| // Convert time value to seconds and return as a number (i.e. float). | |
| // Warning: This function is lossy, i.e. precision is lost when converting | |
| // BigInt -> number. | |
| // Note: BigInt -> number conversion is relatively slow. | |
| static toSeconds(t: time): number { | |
| return Number(t) / TIME_UNITS_PER_SEC; | |
| } | |
| // Convert milliseconds (number) to a time value. | |
| // Note: number -> BigInt conversion is relatively slow. | |
| static fromMillis(millis: number): time { | |
| return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MILLISEC))); | |
| } | |
| // Convert time value to milliseconds and return as a number (i.e. float). | |
| // Warning: This function is lossy, i.e. precision is lost when converting | |
| // BigInt -> number. | |
| // Note: BigInt -> number conversion is relatively slow. | |
| static toMillis(t: time): number { | |
| return Number(t) / TIME_UNITS_PER_MILLISEC; | |
| } | |
| // Convert microseconds (number) to a time value. | |
| // Note: number -> BigInt conversion is relatively slow. | |
| static fromMicros(millis: number): time { | |
| return Time.fromRaw(BigInt(Math.floor(millis * TIME_UNITS_PER_MICROSEC))); | |
| } | |
| // Convert time value to microseconds and return as a number (i.e. float). | |
| // Warning: This function is lossy, i.e. precision is lost when converting | |
| // BigInt -> number. | |
| // Note: BigInt -> number conversion is relatively slow. | |
| static toMicros(t: time): number { | |
| return Number(t) / TIME_UNITS_PER_MICROSEC; | |
| } | |
| /** | |
| * Convert a JavaScript Date to trace time. | |
| * | |
| * @param date - The date. Note that Date objects are time zone agnostic, they | |
| * are a simple wrapper around a unix timestamp. A time zone is applied only | |
| * when extracting data from it such as printing it to a string. | |
| * @param unixOffset - This represents the trace time at the unix epoch | |
| * (usually some large negative number). | |
| * @returns The time represented in trace time. | |
| */ | |
| static fromDate(date: Date, unixOffset: duration): time { | |
| const unixTimeMillis = date.getTime(); | |
| const traceTime = Time.add(Time.fromMillis(unixTimeMillis), unixOffset); | |
| return traceTime; | |
| } | |
| // Convert time value to a Date object, given an offset from the unix epoch. | |
| // Warning: This function is lossy, i.e. precision is lost when converting | |
| // BigInt -> number. | |
| // Note: BigInt -> number conversion is relatively slow. | |
| /** | |
| * Converts trace time to a JavaScript Date object. | |
| * | |
| * @param time - A trace time. | |
| * @param unixOffset - This represents the trace time at the unix epoch | |
| * (usually some large negative number). | |
| * @returns A JavaScript Date object. Remember Date objects don't contain any | |
| * timezone information, they are a simple wrapper around unix time. | |
| */ | |
| static toDate(time: time, unixOffset: duration): Date { | |
| const unixTimeMillis = Time.toMillis(Time.sub(time, unixOffset)); | |
| return new Date(unixTimeMillis); | |
| } | |
| static add(t: time, d: duration): time { | |
| return Time.fromRaw(t + d); | |
| } | |
| static sub(t: time, d: duration): time { | |
| return Time.fromRaw(t - d); | |
| } | |
| static diff(a: time, b: time): duration { | |
| return a - b; | |
| } | |
| // Similar to Time.diff(), but you put the cardinality of the arguments are | |
| // swapped. | |
| // E.g: durationBetween(start, end); | |
| static durationBetween(a: time, b: time): duration { | |
| return b - a; | |
| } | |
| static min(a: time, b: time): time { | |
| return Time.fromRaw(BigintMath.min(a, b)); | |
| } | |
| static max(a: time, b: time): time { | |
| return Time.fromRaw(BigintMath.max(a, b)); | |
| } | |
| static quantFloor(a: time, b: duration): time { | |
| return Time.fromRaw(BigintMath.quantFloor(a, b)); | |
| } | |
| static quantCeil(a: time, b: duration): time { | |
| return Time.fromRaw(BigintMath.quantCeil(a, b)); | |
| } | |
| static quant(a: time, b: duration): time { | |
| return Time.fromRaw(BigintMath.quant(a, b)); | |
| } | |
| static clamp(t: time, min: time, max: time): time { | |
| return Time.fromRaw(BigintMath.clamp(t, min, max)); | |
| } | |
| static formatSeconds(time: time): string { | |
| return Time.toSeconds(time).toString() + ' s'; | |
| } | |
| static formatMilliseconds(time: time): string { | |
| return Time.toMillis(time).toString() + ' ms'; | |
| } | |
| static formatMicroseconds(time: time): string { | |
| return Time.toMicros(time).toString() + ' µs'; | |
| } | |
| static toTimecode(time: time): Timecode { | |
| return new Timecode(time); | |
| } | |
| } | |
| // Format `value` to `n` significant digits. | |
| // Examples: (1234, 2) -> 1234. | |
| // (12.34, 2) -> 12. | |
| // (0.1234, 2) -> 0.12. | |
| function toSignificantDigits(value: number, n: number): string { | |
| const sign = Math.sign(value); | |
| value = Math.abs(value); | |
| // For each of (1, 10, 100, ..., 10^n) we need to render an additional digit | |
| // after comma. | |
| let pow = 1; | |
| let digitsAfterComma = 0; | |
| for (let i = 0; i < n; i++, pow *= 10) { | |
| if (value < pow) { | |
| digitsAfterComma++; | |
| } | |
| } | |
| // Print precisely `digitsAfterComma` digits after comma, unless the number is an integer. | |
| const formatted = | |
| value === Math.round(value) ? value : value.toFixed(digitsAfterComma); | |
| return `${sign < 0 ? '-' : ''}${formatted}`; | |
| } | |
| export class Duration { | |
| // The min and max possible duration values - durations can be negative. | |
| static MIN = BigintMath.INT64_MIN; | |
| static MAX = BigintMath.INT64_MAX; | |
| static ZERO = 0n; | |
| // Cast a bigint to a |duration|. Supports potentially |undefined| values. | |
| // I.e. it performs the following conversions: | |
| // - `bigint` -> `duration` | |
| // - `bigint|undefined` -> `duration|undefined` | |
| // | |
| // Use this function with caution. The function is effectively a no-op in JS, | |
| // but using it tells TypeScript that "this value is a duration value". It's | |
| // up to the caller to ensure the value is in the correct units. | |
| // | |
| // If you're reaching for this function after doing some maths on a |duration| | |
| // value and it's decayed to a |bigint| consider using the static math methods | |
| // in |duration| instead, as they will do the appropriate casting for you. | |
| static fromRaw(v: bigint): duration; | |
| static fromRaw(v?: bigint): duration | undefined; | |
| static fromRaw(v?: bigint): duration | undefined { | |
| return v as duration | undefined; | |
| } | |
| static min(a: duration, b: duration): duration { | |
| return BigintMath.min(a, b); | |
| } | |
| static max(a: duration, b: duration): duration { | |
| return BigintMath.max(a, b); | |
| } | |
| static fromMillis(millis: number) { | |
| return BigInt(Math.floor((millis / 1e3) * TIME_UNITS_PER_SEC)); | |
| } | |
| // Convert time to seconds as a number. | |
| // Use this function with caution. It loses precision and is slow. | |
| static toSeconds(d: duration) { | |
| return Number(d) / TIME_UNITS_PER_SEC; | |
| } | |
| // Convert time to seconds as a number. | |
| // Use this function with caution. It loses precision and is slow. | |
| static toMilliseconds(d: duration) { | |
| return Number(d) / TIME_UNITS_PER_MILLISEC; | |
| } | |
| // Convert time to seconds as a number. | |
| // Use this function with caution. It loses precision and is slow. | |
| static toMicroSeconds(d: duration) { | |
| return Number(d) / TIME_UNITS_PER_MICROSEC; | |
| } | |
| // Print duration as as human readable string - i.e. to only a handful of | |
| // significant figues. | |
| // Use this when readability is more desireable than precision. | |
| // Examples: 1234 -> 1.234us | |
| // 123456789 -> 123.5ms | |
| // 123,123,123,123,123 -> 123123s | |
| // 1,000,000,000 -> 1s | |
| // 1,000,000,023 -> 1.000s | |
| // 1,230,000,023 -> 1.230s | |
| static humanise(dur: duration): string { | |
| if (dur < 1) return '0s'; | |
| const units = ['ns', 'µs', 'ms', 's']; | |
| let n = Math.abs(Number(dur)); | |
| let u = 0; | |
| while (n >= 1000 && u + 1 < units.length) { | |
| n /= 1000; | |
| ++u; | |
| } | |
| return `${toSignificantDigits(Math.sign(Number(dur)) * n, 4)}${units[u]}`; | |
| } | |
| // Print duration with absolute precision. | |
| static format(duration: duration): string { | |
| let result = ''; | |
| if (duration < 1) return '0s'; | |
| const unitAndValue: [string, bigint][] = [ | |
| ['y', 31_536_000_000_000_000n], | |
| ['d', 86_400_000_000_000n], | |
| ['h', 3_600_000_000_000n], | |
| ['m', 60_000_000_000n], | |
| ['s', 1_000_000_000n], | |
| ['ms', 1_000_000n], | |
| ['µs', 1_000n], | |
| ['ns', 1n], | |
| ]; | |
| unitAndValue.forEach(([unit, unitSize]) => { | |
| if (duration >= unitSize) { | |
| const unitCount = duration / unitSize; | |
| result += unitCount.toLocaleString() + unit + ' '; | |
| duration = duration % unitSize; | |
| } | |
| }); | |
| return result.slice(0, -1); | |
| } | |
| static formatSeconds(dur: duration): string { | |
| return Duration.toSeconds(dur).toString() + ' s'; | |
| } | |
| static formatMilliseconds(dur: duration): string { | |
| return Duration.toMilliseconds(dur).toString() + ' ms'; | |
| } | |
| static formatMicroseconds(dur: duration): string { | |
| return Duration.toMicroSeconds(dur).toString() + ' µs'; | |
| } | |
| } | |
| // This class takes a time and converts it to a set of strings representing a | |
| // time code where each string represents a group of time units formatted with | |
| // an appropriate number of leading zeros. | |
| export class Timecode { | |
| public readonly sign: string; | |
| public readonly days: string; | |
| public readonly hours: string; | |
| public readonly minutes: string; | |
| public readonly seconds: string; | |
| public readonly millis: string; | |
| public readonly micros: string; | |
| public readonly nanos: string; | |
| constructor(time: time) { | |
| this.sign = time < 0 ? '-' : ''; | |
| const absTime = BigintMath.abs(time); | |
| const days = absTime / 86_400_000_000_000n; | |
| const hours = (absTime / 3_600_000_000_000n) % 24n; | |
| const minutes = (absTime / 60_000_000_000n) % 60n; | |
| const seconds = (absTime / 1_000_000_000n) % 60n; | |
| const millis = (absTime / 1_000_000n) % 1_000n; | |
| const micros = (absTime / 1_000n) % 1_000n; | |
| const nanos = absTime % 1_000n; | |
| this.days = days.toString(); | |
| this.hours = hours.toString().padStart(2, '0'); | |
| this.minutes = minutes.toString().padStart(2, '0'); | |
| this.seconds = seconds.toString().padStart(2, '0'); | |
| this.millis = millis.toString().padStart(3, '0'); | |
| this.micros = micros.toString().padStart(3, '0'); | |
| this.nanos = nanos.toString().padStart(3, '0'); | |
| } | |
| // Get the upper part of the timecode formatted as: [-]DdHH:MM:SS. | |
| get dhhmmss(): string { | |
| const days = this.days === '0' ? '' : `${this.days}d`; | |
| return `${this.sign}${days}${this.hours}:${this.minutes}:${this.seconds}`; | |
| } | |
| // Get the subsecond part of the timecode formatted as: mmm uuu nnn. | |
| // The "space" char is configurable but defaults to a normal space. | |
| subsec(spaceChar: string = ' '): string { | |
| return `${this.millis}${spaceChar}${this.micros}${spaceChar}${this.nanos}`; | |
| } | |
| // Formats the entire timecode to a string. | |
| toString(spaceChar: string = ' '): string { | |
| return `${this.dhhmmss}.${this.subsec(spaceChar)}`; | |
| } | |
| } | |
| export function currentDateHourAndMinute(): string { | |
| const date = new Date(); | |
| return `${date | |
| .toISOString() | |
| .substr(0, 10)}-${date.getHours()}-${date.getMinutes()}`; | |
| } | |
| export class TimeSpan { | |
| static readonly ZERO = new TimeSpan(Time.ZERO, Time.ZERO); | |
| readonly start: time; | |
| readonly end: time; | |
| constructor(start: time, end: time) { | |
| assertTrue( | |
| start <= end, | |
| `Span start [${start}] cannot be greater than end [${end}]`, | |
| ); | |
| this.start = start; | |
| this.end = end; | |
| } | |
| static fromTimeAndDuration(start: time, duration: duration): TimeSpan { | |
| return new TimeSpan(start, Time.add(start, duration)); | |
| } | |
| get duration(): duration { | |
| return this.end - this.start; | |
| } | |
| get midpoint(): time { | |
| return Time.fromRaw((this.start + this.end) / 2n); | |
| } | |
| contains(t: time): boolean { | |
| return this.start <= t && t < this.end; | |
| } | |
| containsSpan(start: time, end: time): boolean { | |
| return this.start <= start && end <= this.end; | |
| } | |
| overlaps(start: time, end: time): boolean { | |
| return !(end <= this.start || start >= this.end); | |
| } | |
| equals(span: TimeSpan): boolean { | |
| return this.start === span.start && this.end === span.end; | |
| } | |
| translate(x: duration): TimeSpan { | |
| return new TimeSpan(Time.add(this.start, x), Time.add(this.end, x)); | |
| } | |
| pad(padding: duration): TimeSpan { | |
| return new TimeSpan( | |
| Time.sub(this.start, padding), | |
| Time.add(this.end, padding), | |
| ); | |
| } | |
| clamp(min: time, max: time): TimeSpan { | |
| return new TimeSpan( | |
| Time.clamp(this.start, min, max), | |
| Time.clamp(this.end, min, max), | |
| ); | |
| } | |
| } | |
| /** | |
| * Formats a Date object (unix timestamp) to a string in a specified timezone. | |
| * | |
| * @description | |
| * The output format is a customizable combination of `YYYY-MM-DD`, | |
| * `HH:mm:ss.SSS`, and the timezone offset `(±HH:MM)`. | |
| * | |
| * @param date The original JavaScript `Date` object to format. | |
| * @param options An optional configuration object. | |
| * @param {number} [options.tzOffsetMins] - The timezone offset in minutes from | |
| * UTC. For example, -420 for UTC-7 or 330 for UTC+5:30. Defaults to 0 (UTC). | |
| * @param {boolean} [options.printDate] - Whether to include the date part | |
| * (`YYYY-MM-DD`). | |
| * @param {boolean} [options.printTime] - Whether to include the time part | |
| * (`HH:mm:ss.SSS`). | |
| * @param {boolean} [options.printTimezone] - Whether to include the timezone | |
| * offset. | |
| * | |
| * @returns A formatted string representing the date and time in the specified | |
| * timezone. | |
| * | |
| * @example | |
| * const myDate = new Date('2025-06-15T10:00:00.000Z'); | |
| * | |
| * // Format for Indian Standard Time (UTC+5:30) | |
| * // tzOffsetMins = 5 * 60 + 30 = 330 | |
| * const istString = formatDate(myDate, { tzOffsetMins: 330 }); | |
| * console.log(istString); // "2025-06-15 15:30:00.000 UTC+05:30" | |
| * | |
| * // Format for Pacific Daylight Time (UTC-7) | |
| * // tzOffsetMins = -7 * 60 = -420 | |
| * const pdtString = formatDate(myDate, { tzOffsetMins: -420 }); | |
| * console.log(pdtString); // "2025-06-15 03:00:00.000 UTC-07:00" | |
| * | |
| * // Format with only the date and timezone | |
| * const dateOnly = formatDate(myDate, { tzOffsetMins: -420, printTime: false }); | |
| * console.log(dateOnly); // "2025-06-15 UTC-07:00" | |
| * | |
| * // Format as UTC with no timezone part | |
| * const utcOnly = formatDate(myDate, { printTimezone: false }); | |
| * console.log(utcOnly); // "2025-06-15 10:00:00.000" | |
| */ | |
| export function formatDate( | |
| date: Date, | |
| { | |
| tzOffsetMins = 0, | |
| printDate = true, | |
| printTime = true, | |
| printTimezone = true, | |
| }: { | |
| printDate?: boolean; | |
| printTime?: boolean; | |
| printTimezone?: boolean; | |
| tzOffsetMins?: number; | |
| } = {}, | |
| ) { | |
| const originalTimestamp = date.getTime(); | |
| const timezoneOffsetInMilliseconds = tzOffsetMins * 60 * 1000; | |
| const dateInTimezone = new Date( | |
| originalTimestamp + timezoneOffsetInMilliseconds, | |
| ); | |
| const dateStringParts: string[] = []; | |
| if (printDate) { | |
| const year = dateInTimezone.getUTCFullYear(); | |
| const month = String(dateInTimezone.getUTCMonth() + 1).padStart(2, '0'); | |
| const day = String(dateInTimezone.getUTCDate()).padStart(2, '0'); | |
| dateStringParts.push(`${year}-${month}-${day}`); | |
| } | |
| if (printTime) { | |
| const hours = String(dateInTimezone.getUTCHours()).padStart(2, '0'); | |
| const mins = String(dateInTimezone.getUTCMinutes()).padStart(2, '0'); | |
| const sec = String(dateInTimezone.getUTCSeconds()).padStart(2, '0'); | |
| const ms = String(dateInTimezone.getUTCMilliseconds()).padStart(3, '0'); | |
| dateStringParts.push(`${hours}:${mins}:${sec}.${ms}`); | |
| } | |
| if (printTimezone) { | |
| dateStringParts.push(formatTimezone(tzOffsetMins)); | |
| } | |
| return dateStringParts.join(' '); | |
| } | |
| /** | |
| * Given a timezone as an offset from UTC in minutes, format it in the usual ISO | |
| * standard way. E.g.: UTC+01:00 | |
| * | |
| * @param tzOffsetMins - The timezone offset in minutes. | |
| * @returns A string representing the timezone in ISO format. | |
| */ | |
| export function formatTimezone(tzOffsetMins: number): string { | |
| const sign = tzOffsetMins >= 0 ? '+' : '-'; | |
| const absMins = Math.abs(tzOffsetMins); | |
| const hours = Math.floor(absMins / 60); | |
| const mins = absMins % 60; | |
| const hoursStr = String(hours).padStart(2, '0'); | |
| const minsStr = String(mins).padStart(2, '0'); | |
| return `UTC${sign}${hoursStr}:${minsStr}`; | |
| } | |
| /** | |
| * A TypeScript Map that pairs user-friendly timezone descriptions | |
| * with their corresponding UTC offset in minutes. | |
| */ | |
| export const timezoneOffsetMap: {[key: string]: number} = { | |
| '(UTC-12:00) International Date Line West': -720, | |
| '(UTC-11:00) Coordinated Universal Time-11': -660, | |
| '(UTC-10:00) Hawaii': -600, | |
| '(UTC-09:30) Marquesas Islands': -570, | |
| '(UTC-09:00) Alaska': -540, | |
| '(UTC-08:00) Pacific Time (US & Canada)': -480, | |
| '(UTC-07:00) Mountain Time (US & Canada)': -420, | |
| '(UTC-06:00) Central Time (US & Canada), Mexico City': -360, | |
| '(UTC-05:00) Eastern Time (US & Canada), Bogota, Lima': -300, | |
| '(UTC-04:00) Atlantic Time (Canada), La Paz': -240, | |
| '(UTC-03:30) Newfoundland': -210, | |
| '(UTC-03:00) Buenos Aires, São Paulo': -180, | |
| '(UTC-02:00) Coordinated Universal Time-02': -120, | |
| '(UTC-01:00) Azores, Cape Verde Is.': -60, | |
| '(UTC+00:00) London, Dublin, Lisbon, Casablanca': 0, | |
| '(UTC+01:00) Amsterdam, Berlin, Paris, Rome, Madrid': 60, | |
| '(UTC+02:00) Athens, Cairo, Johannesburg, Helsinki': 120, | |
| '(UTC+03:00) Moscow, Istanbul, Riyadh, Nairobi': 180, | |
| '(UTC+03:30) Tehran': 210, | |
| '(UTC+04:00) Dubai, Abu Dhabi, Muscat, Baku': 240, | |
| '(UTC+04:30) Kabul': 270, | |
| '(UTC+05:00) Karachi, Tashkent': 300, | |
| '(UTC+05:30) Mumbai, New Delhi, Kolkata, Colombo': 330, | |
| '(UTC+05:45) Kathmandu': 345, | |
| '(UTC+06:00) Almaty, Dhaka': 360, | |
| '(UTC+06:30) Yangon (Rangoon)': 390, | |
| '(UTC+07:00) Bangkok, Hanoi, Jakarta': 420, | |
| '(UTC+08:00) Beijing, Hong Kong, Singapore, Taipei, Perth': 480, | |
| '(UTC+08:45) Eucla': 525, | |
| '(UTC+09:00) Tokyo, Seoul, Osaka, Sapporo': 540, | |
| '(UTC+09:30) Adelaide, Darwin': 570, | |
| '(UTC+10:00) Sydney, Melbourne, Brisbane, Guam': 600, | |
| '(UTC+10:30) Lord Howe Island': 630, | |
| '(UTC+11:00) Solomon Is., New Caledonia': 660, | |
| '(UTC+12:00) Auckland, Wellington, Fiji': 720, | |
| '(UTC+12:45) Chatham Islands': 765, | |
| "(UTC+13:00) Nuku'alofa": 780, | |
| '(UTC+14:00) Kiritimati': 840, | |
| }; |