diff --git a/packages/core/src/__tests__/time.test.ts b/packages/core/src/__tests__/time.test.ts new file mode 100644 index 0000000000..a2cd245e57 --- /dev/null +++ b/packages/core/src/__tests__/time.test.ts @@ -0,0 +1,398 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { Timestamp } from '../classes' +import { getDay, convertToDay } from '..' + +const supportedTimezones: string[] = [ + 'Europe/Andorra', + 'Asia/Dubai', + 'Asia/Kabul', + 'Europe/Tirane', + 'Asia/Yerevan', + 'Antarctica/Casey', + 'Antarctica/Davis', + 'Antarctica/DumontDUrville', + 'Antarctica/Mawson', + 'Antarctica/Palmer', + 'Antarctica/Rothera', + 'Antarctica/Syowa', + 'Antarctica/Troll', + 'Antarctica/Vostok', + 'America/Argentina/Buenos_Aires', + 'America/Argentina/Cordoba', + 'America/Argentina/Salta', + 'America/Argentina/Jujuy', + 'America/Argentina/Tucuman', + 'America/Argentina/Catamarca', + 'America/Argentina/La_Rioja', + 'America/Argentina/San_Juan', + 'America/Argentina/Mendoza', + 'America/Argentina/San_Luis', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Ushuaia', + 'Pacific/Pago_Pago', + 'Europe/Vienna', + 'Australia/Lord_Howe', + 'Antarctica/Macquarie', + 'Australia/Hobart', + 'Australia/Currie', + 'Australia/Melbourne', + 'Australia/Sydney', + 'Australia/Broken_Hill', + 'Australia/Brisbane', + 'Australia/Lindeman', + 'Australia/Adelaide', + 'Australia/Darwin', + 'Australia/Perth', + 'Australia/Eucla', + 'Asia/Baku', + 'America/Barbados', + 'Asia/Dhaka', + 'Europe/Brussels', + 'Europe/Sofia', + 'Atlantic/Bermuda', + 'Asia/Brunei', + 'America/La_Paz', + 'America/Noronha', + 'America/Belem', + 'America/Fortaleza', + 'America/Recife', + 'America/Araguaina', + 'America/Maceio', + 'America/Bahia', + 'America/Sao_Paulo', + 'America/Campo_Grande', + 'America/Cuiaba', + 'America/Santarem', + 'America/Porto_Velho', + 'America/Boa_Vista', + 'America/Manaus', + 'America/Eirunepe', + 'America/Rio_Branco', + 'America/Nassau', + 'Asia/Thimphu', + 'Europe/Minsk', + 'America/Belize', + 'America/St_Johns', + 'America/Halifax', + 'America/Glace_Bay', + 'America/Moncton', + 'America/Goose_Bay', + 'America/Blanc-Sablon', + 'America/Toronto', + 'America/Nipigon', + 'America/Thunder_Bay', + 'America/Iqaluit', + 'America/Pangnirtung', + 'America/Atikokan', + 'America/Winnipeg', + 'America/Rainy_River', + 'America/Resolute', + 'America/Rankin_Inlet', + 'America/Regina', + 'America/Swift_Current', + 'America/Edmonton', + 'America/Cambridge_Bay', + 'America/Yellowknife', + 'America/Inuvik', + 'America/Creston', + 'America/Dawson_Creek', + 'America/Fort_Nelson', + 'America/Vancouver', + 'America/Whitehorse', + 'America/Dawson', + 'Indian/Cocos', + 'Europe/Zurich', + 'Africa/Abidjan', + 'Pacific/Rarotonga', + 'America/Santiago', + 'America/Punta_Arenas', + 'Pacific/Easter', + 'Asia/Shanghai', + 'Asia/Urumqi', + 'America/Bogota', + 'America/Costa_Rica', + 'America/Havana', + 'Atlantic/Cape_Verde', + 'America/Curacao', + 'Indian/Christmas', + 'Asia/Nicosia', + 'Asia/Famagusta', + 'Europe/Prague', + 'Europe/Berlin', + 'Europe/Copenhagen', + 'America/Santo_Domingo', + 'Africa/Algiers', + 'America/Guayaquil', + 'Pacific/Galapagos', + 'Europe/Tallinn', + 'Africa/Cairo', + 'Africa/El_Aaiun', + 'Europe/Madrid', + 'Africa/Ceuta', + 'Atlantic/Canary', + 'Europe/Helsinki', + 'Atlantic/Stanley', + 'Pacific/Chuuk', + 'Pacific/Pohnpei', + 'Atlantic/Faroe', + 'Europe/Paris', + 'Europe/London', + 'Asia/Tbilisi', + 'America/Cayenne', + 'Africa/Accra', + 'Europe/Gibraltar', + 'America/Godthab', + 'America/Danmarkshavn', + 'America/Scoresbysund', + 'America/Thule', + 'Europe/Athens', + 'Atlantic/South_Georgia', + 'America/Guatemala', + 'Pacific/Guam', + 'Africa/Bissau', + 'America/Guyana', + 'Asia/Hong_Kong', + 'America/Tegucigalpa', + 'America/Port-au-Prince', + 'Europe/Budapest', + 'Asia/Jakarta', + 'Asia/Pontianak', + 'Asia/Makassar', + 'Asia/Jayapura', + 'Europe/Dublin', + 'Asia/Jerusalem', + 'Asia/Kolkata', + 'Indian/Chagos', + 'Asia/Baghdad', + 'Asia/Tehran', + 'Atlantic/Reykjavik', + 'Europe/Rome', + 'America/Jamaica', + 'Asia/Amman', + 'Asia/Tokyo', + 'Africa/Nairobi', + 'Asia/Bishkek', + 'Asia/Pyongyang', + 'Asia/Seoul', + 'Asia/Almaty', + 'Asia/Qyzylorda', + 'Asia/Qostanay', + 'Asia/Aqtobe', + 'Asia/Aqtau', + 'Asia/Atyrau', + 'Asia/Oral', + 'Asia/Beirut', + 'Asia/Colombo', + 'Africa/Monrovia', + 'Europe/Vilnius', + 'Europe/Luxembourg', + 'Europe/Riga', + 'Africa/Tripoli', + 'Africa/Casablanca', + 'Europe/Monaco', + 'Europe/Chisinau', + 'Asia/Yangon', + 'Asia/Ulaanbaatar', + 'Asia/Hovd', + 'Asia/Choibalsan', + 'Asia/Macau', + 'America/Martinique', + 'Europe/Malta', + 'Indian/Mauritius', + 'Indian/Maldives', + 'America/Mexico_City', + 'America/Cancun', + 'America/Merida', + 'America/Monterrey', + 'America/Matamoros', + 'America/Mazatlan', + 'America/Chihuahua', + 'America/Ojinaga', + 'America/Hermosillo', + 'America/Tijuana', + 'America/Bahia_Banderas', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Africa/Maputo', + 'Africa/Windhoek', + 'Pacific/Noumea', + 'Africa/Lagos', + 'America/Managua', + 'Europe/Amsterdam', + 'Europe/Oslo', + 'Asia/Kathmandu', + 'Pacific/Niue', + 'America/Panama', + 'America/Lima', + 'Pacific/Tahiti', + 'Pacific/Marquesas', + 'Pacific/Gambier', + 'Pacific/Port_Moresby', + 'Pacific/Bougainville', + 'Asia/Manila', + 'Asia/Karachi', + 'Europe/Warsaw', + 'America/Miquelon', + 'Pacific/Pitcairn', + 'America/Puerto_Rico', + 'Asia/Gaza', + 'Asia/Hebron', + 'Europe/Lisbon', + 'Atlantic/Madeira', + 'Atlantic/Azores', + 'Pacific/Palau', + 'America/Asuncion', + 'Asia/Qatar', + 'Indian/Reunion', + 'Europe/Bucharest', + 'Europe/Belgrade', + 'Europe/Kaliningrad', + 'Europe/Moscow', + 'Europe/Simferopol', + 'Europe/Kirov', + 'Europe/Astrakhan', + 'Europe/Volgograd', + 'Europe/Saratov', + 'Europe/Ulyanovsk', + 'Europe/Samara', + 'Asia/Yekaterinburg', + 'Asia/Omsk', + 'Asia/Novosibirsk', + 'Asia/Barnaul', + 'Asia/Tomsk', + 'Asia/Novokuznetsk', + 'Asia/Krasnoyarsk', + 'Asia/Irkutsk', + 'Asia/Chita', + 'Asia/Yakutsk', + 'Asia/Khandyga', + 'Asia/Vladivostok', + 'Asia/Ust-Nera', + 'Asia/Magadan', + 'Asia/Sakhalin', + 'Asia/Srednekolymsk', + 'Asia/Riyadh', + 'Pacific/Guadalcanal', + 'Indian/Mahe', + 'Africa/Khartoum', + 'Europe/Stockholm', + 'Asia/Singapore', + 'America/Paramaribo', + 'Africa/Juba', + 'Africa/Sao_Tome', + 'America/El_Salvador', + 'Asia/Damascus', + 'America/Grand_Turk', + 'Africa/Ndjamena', + 'Indian/Kerguelen', + 'Asia/Bangkok', + 'Asia/Dushanbe', + 'Asia/Dili', + 'Asia/Ashgabat', + 'Africa/Tunis', + 'Europe/Istanbul', + 'America/Port_of_Spain', + 'Asia/Taipei', + 'Europe/Kiev', + 'Europe/Uzhgorod', + 'Europe/Zaporozhye', + 'America/New_York', + 'America/Detroit', + 'America/Kentucky/Louisville', + 'America/Kentucky/Monticello', + 'America/Indiana/Indianapolis', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Indiana/Marengo', + 'America/Indiana/Petersburg', + 'America/Indiana/Vevay', + 'America/Chicago', + 'America/Indiana/Tell_City', + 'America/Indiana/Knox', + 'America/Menominee', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/North_Dakota/Beulah', + 'America/Denver', + 'America/Boise', + 'America/Phoenix', + 'America/Los_Angeles', + 'America/Anchorage', + 'America/Juneau', + 'America/Sitka', + 'America/Metlakatla', + 'America/Yakutat', + 'America/Nome', + 'America/Adak', + 'Pacific/Honolulu', + 'America/Montevideo', + 'Asia/Samarkand', + 'Asia/Tashkent', + 'America/Caracas', + 'Asia/Ho_Chi_Minh', + 'Pacific/Efate', + 'Africa/Johannesburg' +] + +function testGetDay (date: Date, timezone: string): void { + const timestamp: Timestamp = getDay(date.getTime()) + const convertedDate: Date = new Date(timestamp) + const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) + const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) + expect(convertedLocaleDate).toEqual(originalLocaleDate) +} + +function testConvertToDay (date: Date, timezone: string): void { + const convertedDate: Date = convertToDay(date) + const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' }) + const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone }) + expect(convertedLocaleDate).toEqual(originalLocaleDate) +} + +describe('time', () => { + it.each(supportedTimezones)('dates are matched in `getDay` for locale %p', (timezone: string) => { + testGetDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone) + testGetDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone) + testGetDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone) + testGetDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone) + testGetDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone) + }) + + it.each(supportedTimezones)('dates are matched in `convertToDay` for locale %p', (timezone: string) => { + testConvertToDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone) + testConvertToDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone) + testConvertToDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone) + testConvertToDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone) + testConvertToDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone) + }) + + it.each([ + [0, 0, 0, 0], + [7, 59, 59, 999], + [12, 0, 0, 0], + [23, 59, 59, 999] + ])( + 'dates are matched for time [h: %p, m: %p, s: %p, ms: %p]', + (hours: number, minutes: number, seconds: number, milliSeconds: number) => { + const date: Date = new Date() + const expectedDay: number = date.getDate() + date.setHours(hours, minutes, seconds, milliSeconds) + const convertedDate: Date = convertToDay(date) + expect(convertedDate.getDate()).toEqual(expectedDay) + } + ) +}) diff --git a/packages/core/src/time.ts b/packages/core/src/time.ts index 5cb82eb0d9..ec6aa7d62c 100644 --- a/packages/core/src/time.ts +++ b/packages/core/src/time.ts @@ -16,8 +16,18 @@ import { Timestamp } from './classes' export function getDay (time: Timestamp): Timestamp { const date: Date = new Date(time) - date.setHours(0, 0, 0, 0) - return date.getTime() + return convertToDay(date).getTime() +} + +export function convertToDay (date: Date): Date { + const originalDay: number = date.getDate() + const convertedDate: Date = new Date(date) + // Set 12 AM UTC time, since it will be the same day in most timezones + convertedDate.setUTCHours(12, 0, 0, 0) + if (convertedDate.getDate() !== originalDay) { + convertedDate.setDate(originalDay) + } + return convertedDate } export function getHour (time: Timestamp): Timestamp { diff --git a/packages/ui/src/components/calendar/DatePopup.svelte b/packages/ui/src/components/calendar/DatePopup.svelte index d0a3254752..307762d726 100644 --- a/packages/ui/src/components/calendar/DatePopup.svelte +++ b/packages/ui/src/components/calendar/DatePopup.svelte @@ -13,7 +13,7 @@ // limitations under the License. -->