diff --git a/pages/date-range-picker/absolute-format.localization.page.tsx b/pages/date-range-picker/absolute-format.localization.page.tsx new file mode 100644 index 0000000000..697538aad8 --- /dev/null +++ b/pages/date-range-picker/absolute-format.localization.page.tsx @@ -0,0 +1,126 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useContext, useState } from 'react'; +import { Box, DateRangePicker, DateRangePickerProps, SpaceBetween, Grid } from '~components'; +import { i18nStrings, isValid } from './common'; +import AppContext, { AppContextType } from '../app/app-context'; + +const locales = [ + 'ar', + 'de', + 'en-GB', + 'en-US', + 'es', + 'fr', + 'he', + 'id', + 'it', + 'ja', + 'ko', + 'ms', + 'pt-BR', + 'th', + 'tr', + 'vi', + 'zh-CN', + 'zh-TW', +]; + +const rtlLocales = new Set(['ar', 'he']); + +type DemoContext = React.Context< + AppContextType<{ + absoluteFormat?: DateRangePickerProps.AbsoluteFormat; + dateOnly?: boolean; + hideTimeOffset?: boolean; + timeOffset?: number; + }> +>; + +const initialRange = { + startDate: '2024-12-09T00:00:00+01:00', + endDate: '2024-12-31T23:59:59+01:00', +}; + +export default function DateRangePickerScenario() { + const { urlParams, setUrlParams } = useContext(AppContext as DemoContext); + + const [value, setValue] = useState({ + type: 'absolute', + startDate: urlParams.dateOnly ? initialRange.startDate.slice(0, 10) : initialRange.startDate, + endDate: urlParams.dateOnly ? initialRange.endDate.slice(0, 10) : initialRange.endDate, + }); + + return ( + + +

Absolute date range picker with custom absolute format

+ + + + + + +
+ {locales.map(locale => ( +
+ +
{locale}
+ setValue(e.detail.value)} + relativeOptions={[]} + isValidRange={isValid} + rangeSelectorMode={'absolute-only'} + getTimeOffset={urlParams.timeOffset === undefined ? undefined : () => urlParams.timeOffset!} + absoluteFormat={urlParams.absoluteFormat} + dateOnly={urlParams.dateOnly} + hideTimeOffset={urlParams.hideTimeOffset} + /> +
+
+ ))} +
+
+ ); +} diff --git a/pages/date-range-picker/absolute-format.permutations.page.tsx b/pages/date-range-picker/absolute-format.permutations.page.tsx new file mode 100644 index 0000000000..6b24456380 --- /dev/null +++ b/pages/date-range-picker/absolute-format.permutations.page.tsx @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { Box, DateRangePicker, DateRangePickerProps, SpaceBetween } from '~components'; +import { i18nStrings, isValid } from './common'; +import ScreenshotArea from '../utils/screenshot-area'; +import PermutationsView from '../utils/permutations-view'; +import createPermutations from '../utils/permutations'; + +const permutations = createPermutations< + Pick +>([ + { + absoluteFormat: ['iso', 'long-localized'], + dateOnly: [true], + value: [ + { + type: 'absolute', + startDate: '2024-12-30', + endDate: '2024-12-31', + }, + ], + }, + { + absoluteFormat: ['iso', 'long-localized'], + dateOnly: [false], + hideTimeOffset: [true, false], + value: [ + { + type: 'absolute', + startDate: '2024-12-30T00:00:00+01:00', + endDate: '2024-12-31T23:59:59+01:00', + }, + ], + }, +]); + +export default function DateRangePickerPermutations() { + return ( + + +

Absolute date range picker with custom absolute format

+
+ + ( + 60} + /> + )} + /> + +
+
+ ); +} diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 2fef491f6c..1acd95b105 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -6009,6 +6009,27 @@ The event \`detail\` contains the current value of the field.", ], "name": "DateRangePicker", "properties": Array [ + Object { + "defaultValue": "'iso'", + "description": "Specifies the time format to use for displaying the absolute time range. +It can take the following values: +* \`iso\`: ISO 8601 format, e.g.: 2024-01-30T13:32:32+01:00 (or 2024-01-30 when \`dateOnly\` is true) +* \`long-localized\`: a more human-readable, localized format, e.g.: January 30, 2024, 13:32:32 (UTC+1) (or January 30, 2024 when \`dateOnly\` is true) + +Defaults to \`iso\`. +", + "inlineType": Object { + "name": "DateRangePickerProps.AbsoluteFormat", + "type": "union", + "values": Array [ + "iso", + "long-localized", + ], + }, + "name": "absoluteFormat", + "optional": true, + "type": "string", + }, Object { "description": "Adds \`aria-describedby\` to the component. If you're using this component within a form field, don't set this property because the form field component automatically sets it. @@ -6132,6 +6153,13 @@ Default: the user's current time offset as provided by the browser. "optional": true, "type": "DateRangePickerProps.GetTimeOffsetFunction", }, + Object { + "description": "Specifies whether to hide the time offset in the displayed absolute time range. +Defaults to \`false\`.", + "name": "hideTimeOffset", + "optional": true, + "type": "boolean", + }, Object { "description": "An object containing all the necessary localized strings required by the component.", "i18nTag": true, diff --git a/src/date-range-picker/index.tsx b/src/date-range-picker/index.tsx index 780c2eaef0..3108ce5725 100644 --- a/src/date-range-picker/index.tsx +++ b/src/date-range-picker/index.tsx @@ -34,12 +34,23 @@ import ResetContextsForModal from '../internal/context/reset-contexts-for-modal. export { DateRangePickerProps }; -function renderDateRange( - range: null | DateRangePickerProps.Value, - placeholder: string, - formatRelativeRange: DateRangePickerProps.I18nStrings['formatRelativeRange'], - timeOffset: { startDate?: number; endDate?: number } -) { +function renderDateRange({ + locale, + range, + placeholder = '', + formatRelativeRange, + absoluteFormat, + hideTimeOffset, + timeOffset, +}: { + locale?: string; + range: null | DateRangePickerProps.Value; + placeholder?: string; + formatRelativeRange: DateRangePickerProps.I18nStrings['formatRelativeRange']; + absoluteFormat: DateRangePickerProps.AbsoluteFormat; + hideTimeOffset?: boolean; + timeOffset: { startDate?: number; endDate?: number }; +}) { if (!range) { return ( @@ -52,7 +63,16 @@ function renderDateRange( range.type === 'relative' ? ( formatRelativeRange?.(range) ?? '' ) : ( - + ); return ( @@ -107,6 +127,8 @@ const DateRangePicker = React.forwardRef( expandToViewport = false, rangeSelectorMode = 'default', customAbsoluteRangeControl, + absoluteFormat = 'iso', + hideTimeOffset, ...rest }: DateRangePickerProps, ref: Ref @@ -229,6 +251,16 @@ const DateRangePicker = React.forwardRef( } } + const formattedDate: string | JSX.Element = renderDateRange({ + locale: normalizedLocale, + range: value, + placeholder, + formatRelativeRange, + absoluteFormat, + hideTimeOffset, + timeOffset: normalizedTimeOffset, + }); + const trigger = (
- - {renderDateRange(value, placeholder ?? '', formatRelativeRange, normalizedTimeOffset)} - + {formattedDate}
@@ -269,7 +299,11 @@ const DateRangePicker = React.forwardRef(
string; } + + export type AbsoluteFormat = 'iso' | 'long-localized'; } export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; diff --git a/src/date-range-picker/styles.scss b/src/date-range-picker/styles.scss index b9fda09b72..e4f97e728c 100644 --- a/src/date-range-picker/styles.scss +++ b/src/date-range-picker/styles.scss @@ -14,7 +14,12 @@ $calendar-header-color: awsui.$color-text-body-default; .root { @include styles.styles-reset; - max-inline-size: 32em; + &:not(.wide) { + max-inline-size: 32em; + } + &.wide { + max-inline-size: 39em; + } } .focus-lock { diff --git a/src/date-range-picker/time-offset.ts b/src/date-range-picker/time-offset.ts index 8298d74885..fa69669648 100644 --- a/src/date-range-picker/time-offset.ts +++ b/src/date-range-picker/time-offset.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { DateRangePickerProps } from './interfaces'; import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; -import { formatTimezoneOffset, parseTimezoneOffset, shiftTimezoneOffset } from '../internal/utils/date-time'; +import { formatTimeOffsetISO, parseTimezoneOffset, shiftTimezoneOffset } from '../internal/utils/date-time'; import { addMinutes } from 'date-fns'; /** @@ -17,8 +17,8 @@ export function setTimeOffset( } return { type: 'absolute', - startDate: value.startDate + formatTimezoneOffset(value.startDate, timeOffset.startDate), - endDate: value.endDate + formatTimezoneOffset(value.endDate, timeOffset.endDate), + startDate: value.startDate + formatTimeOffsetISO(value.startDate, timeOffset.startDate), + endDate: value.endDate + formatTimeOffsetISO(value.endDate, timeOffset.endDate), }; } diff --git a/src/internal/utils/date-time/__tests__/format-date-range.test.ts b/src/internal/utils/date-time/__tests__/format-date-range.test.ts index 59a5e1e53c..17e3811f45 100644 --- a/src/internal/utils/date-time/__tests__/format-date-range.test.ts +++ b/src/internal/utils/date-time/__tests__/format-date-range.test.ts @@ -9,13 +9,171 @@ const newYork = { startDate: -240, endDate: -240 }; const regional = { startDate: 0, endDate: 60 }; describe('formatDateRange', () => { - test.each([ - ['2020-01-01', '2020-01-02', browser, '2020-01-01 — 2020-01-02'], - ['2020-01-01', '2020-01-02', berlin, '2020-01-01 — 2020-01-02'], - ['2020-01-01T00:00:00', '2020-01-01T12:00:00', berlin, '2020-01-01T00:00:00+02:00 — 2020-01-01T12:00:00+02:00'], - ['2020-01-01T00:00:00', '2020-01-01T12:00:00', newYork, '2020-01-01T00:00:00-04:00 — 2020-01-01T12:00:00-04:00'], - ['2020-01-01T00:00:00', '2020-01-01T12:00:00', regional, '2020-01-01T00:00:00+00:00 — 2020-01-01T12:00:00+01:00'], - ])('formats date correctly [%s, %s, %s]', (startDate, endDate, timeOffset, expected) => { - expect(formatDateRange(startDate, endDate, timeOffset)).toBe(expected); + describe('Only date', () => { + const cases = [ + { + startDate: '2020-01-01', + endDate: '2020-01-02', + timeOffset: browser, + expected: { + iso: '2020-01-01 — 2020-01-02', + localized: { 'en-US': 'January 1, 2020 — January 2, 2020' }, + }, + }, + { + startDate: '2020-01-01', + endDate: '2020-01-02', + timeOffset: berlin, + expected: { iso: '2020-01-01 — 2020-01-02', localized: { 'en-US': 'January 1, 2020 — January 2, 2020' } }, + }, + ]; + describe.each(cases)( + 'formats date correctly [startDate=$startDate, endDate=$endDate, timeOffset=$timeOffset]', + ({ startDate, endDate, timeOffset, expected }) => { + test('ISO', () => { + expect(formatDateRange({ startDate, endDate, timeOffset, format: 'iso' })).toBe(expected.iso); + }); + test('Human-readable', () => { + expect(formatDateRange({ startDate, endDate, timeOffset, format: 'long-localized', locale: 'en-US' })).toBe( + expected.localized['en-US'] + ); + }); + } + ); + }); + + describe('Date and time', () => { + describe('with time offset', () => { + const cases = [ + { + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: berlin, + expected: { + iso: '2020-01-01T00:00:00+02:00 — 2020-01-01T12:00:00+02:00', + localized: { 'en-US': 'January 1, 2020, 00:00:00 (UTC+2) — January 1, 2020, 12:00:00 (UTC+2)' }, + }, + }, + { + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: newYork, + expected: { + iso: '2020-01-01T00:00:00-04:00 — 2020-01-01T12:00:00-04:00', + localized: { 'en-US': 'January 1, 2020, 00:00:00 (UTC-4) — January 1, 2020, 12:00:00 (UTC-4)' }, + }, + }, + { + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: regional, + expected: { + iso: '2020-01-01T00:00:00+00:00 — 2020-01-01T12:00:00+01:00', + localized: { 'en-US': 'January 1, 2020, 00:00:00 (UTC) — January 1, 2020, 12:00:00 (UTC+1)' }, + }, + }, + ]; + describe.each(cases)( + 'formats date correctly [startDate=$startDate, endDate=$endDate, timeOffset=$timeOffset]', + ({ startDate, endDate, timeOffset, expected }) => { + test('ISO', () => { + expect(formatDateRange({ startDate, endDate, timeOffset, format: 'iso' })).toBe(expected.iso); + }); + test('Human-readable', () => { + expect(formatDateRange({ startDate, endDate, timeOffset, format: 'long-localized', locale: 'en-US' })).toBe( + expected.localized['en-US'] + ); + }); + } + ); + }); + + describe('without time offset', () => { + const cases = [ + { + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: berlin, + expected: { + iso: '2020-01-01T00:00:00 — 2020-01-01T12:00:00', + localized: { 'en-US': 'January 1, 2020, 00:00:00 — January 1, 2020, 12:00:00' }, + }, + }, + { + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: newYork, + expected: { + iso: '2020-01-01T00:00:00 — 2020-01-01T12:00:00', + localized: { 'en-US': 'January 1, 2020, 00:00:00 — January 1, 2020, 12:00:00' }, + }, + }, + { + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: regional, + expected: { + iso: '2020-01-01T00:00:00 — 2020-01-01T12:00:00', + localized: { 'en-US': 'January 1, 2020, 00:00:00 — January 1, 2020, 12:00:00' }, + }, + }, + ]; + + describe.each(cases)( + 'formats date correctly [startDate=$startDate, endDate=$endDate, timeOffset=$timeOffset]', + ({ startDate, endDate, timeOffset, expected }) => { + test('ISO', () => { + expect(formatDateRange({ startDate, endDate, timeOffset, hideTimeOffset: true, format: 'iso' })).toBe( + expected.iso + ); + }); + test('Human-readable', () => { + expect( + formatDateRange({ + startDate, + endDate, + timeOffset, + hideTimeOffset: true, + format: 'long-localized', + locale: 'en-US', + }) + ).toBe(expected.localized['en-US']); + }); + } + ); + }); + }); + + describe('Localization', () => { + describe('uses comma to separate date and time in some languages', () => { + test.each(['ar', 'de', 'en-GB', 'en-US', 'es', 'fr', 'he', 'id', 'it', 'ko', 'pt-BR', 'th', 'tr'])( + '%s', + locale => { + expect( + formatDateRange({ + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: { startDate: 60 }, + locale, + format: 'long-localized', + }) + ).toContain(', '); + } + ); + }); + + describe('does not use comma to separate date and time in some languages', () => { + test.each(['ja', 'zh-CN', 'zh-TW'])('%s', locale => { + expect( + formatDateRange({ + startDate: '2020-01-01T00:00:00', + endDate: '2020-01-01T12:00:00', + timeOffset: { startDate: 60 }, + locale, + format: 'long-localized', + }) + ).not.toContain(','); + }); + }); }); }); diff --git a/src/internal/utils/date-time/__tests__/format-timezone-offset.test.ts b/src/internal/utils/date-time/__tests__/format-time-offset.test.ts similarity index 65% rename from src/internal/utils/date-time/__tests__/format-timezone-offset.test.ts rename to src/internal/utils/date-time/__tests__/format-time-offset.test.ts index 01a3e29760..f1669ea091 100644 --- a/src/internal/utils/date-time/__tests__/format-timezone-offset.test.ts +++ b/src/internal/utils/date-time/__tests__/format-time-offset.test.ts @@ -1,11 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { formatTimezoneOffset } from '../../../../../lib/components/internal/utils/date-time/format-timezone-offset'; +import { formatTimeOffsetISO } from '../../../../../lib/components/internal/utils/date-time/format-time-offset'; -test('formatTimezoneOffset', () => { +test('formatTimeOffsetISO', () => { for (let offset = -120; offset <= 120; offset++) { - const formatted = formatTimezoneOffset('2020-01-01', offset); + const formatted = formatTimeOffsetISO('2020-01-01', offset); const sign = Number(formatted[0] + '1'); const hours = Number(formatted[1] + formatted[2]); const minutes = Number(formatted[4] + formatted[5]); @@ -16,5 +16,5 @@ test('formatTimezoneOffset', () => { test.each(['2020-01-01', '2020-06-01'])('uses browser offset by default [%s]', isoDate => { const offset = 0 - new Date(isoDate).getTimezoneOffset(); - expect(formatTimezoneOffset(isoDate)).toBe(formatTimezoneOffset(isoDate, offset)); + expect(formatTimeOffsetISO(isoDate)).toBe(formatTimeOffsetISO(isoDate, offset)); }); diff --git a/src/internal/utils/date-time/format-date-iso.ts b/src/internal/utils/date-time/format-date-iso.ts new file mode 100644 index 0000000000..7db1327749 --- /dev/null +++ b/src/internal/utils/date-time/format-date-iso.ts @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { formatTimeOffsetISO } from './format-time-offset'; + +export default function ({ + date: isoDate, + hideTimeOffset, + isDateOnly, + timeOffset, +}: { + date: string; + hideTimeOffset?: boolean; + isDateOnly: boolean; + timeOffset?: number; +}) { + const formattedOffset = hideTimeOffset || isDateOnly ? '' : formatTimeOffsetISO(isoDate, timeOffset); + return isoDate + formattedOffset; +} diff --git a/src/internal/utils/date-time/format-date-localized.ts b/src/internal/utils/date-time/format-date-localized.ts new file mode 100644 index 0000000000..86bee688a3 --- /dev/null +++ b/src/internal/utils/date-time/format-date-localized.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { formatTimeOffsetLocalized } from './format-time-offset'; + +export default function formatDateLocalized({ + date: isoDate, + hideTimeOffset, + isDateOnly, + timeOffset, + locale, +}: { + date: string; + hideTimeOffset?: boolean; + isDateOnly: boolean; + timeOffset?: number; + locale?: string; +}) { + const date = new Date(isoDate); + + const formattedDate = new Intl.DateTimeFormat(locale, { + day: 'numeric', + month: 'long', + year: 'numeric', + }).format(date); + + if (isDateOnly) { + return formattedDate; + } + + const formattedTime = new Intl.DateTimeFormat(locale, { + hour: '2-digit', + hourCycle: 'h23', + minute: '2-digit', + second: '2-digit', + }).format(date); + + const formattedDateTime = formattedDate + getDateTimeSeparator(locale) + formattedTime; + + if (hideTimeOffset) { + return formattedDateTime; + } + + const formattedTimeOffset = formatTimeOffsetLocalized(isoDate, timeOffset); + return formattedDateTime + ' ' + formattedTimeOffset; +} + +// Languages in which date and time are separated just with a space, without comma +const languagesWithoutDateTimeSeparator = ['ja', 'zh-CN', 'zh-TW']; + +function getDateTimeSeparator(locale?: string) { + return locale && languagesWithoutDateTimeSeparator.includes(locale) ? ' ' : ', '; +} diff --git a/src/internal/utils/date-time/format-date-range.ts b/src/internal/utils/date-time/format-date-range.ts index 78f3a44549..3c7b5d0cdf 100644 --- a/src/internal/utils/date-time/format-date-range.ts +++ b/src/internal/utils/date-time/format-date-range.ts @@ -1,16 +1,69 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { formatTimezoneOffset } from './format-timezone-offset'; import { isIsoDateOnly } from './is-iso-date-only'; +import formatDateIso from './format-date-iso'; +import formatDateLocalized from './format-date-localized'; +import { DateRangePickerProps } from '../../../date-range-picker/interfaces'; -export function formatDateRange( - startDate: string, - endDate: string, - timeOffset: { startDate?: number; endDate?: number } -): string { +export function formatDateRange({ + startDate, + endDate, + timeOffset, + hideTimeOffset, + format, + locale, +}: { + startDate: string; + endDate: string; + hideTimeOffset?: boolean; + timeOffset: { startDate?: number; endDate?: number }; + format: DateRangePickerProps.AbsoluteFormat; + locale?: string; +}): string { const isDateOnly = isIsoDateOnly(startDate) && isIsoDateOnly(endDate); - const formattedStartOffset = isDateOnly ? '' : formatTimezoneOffset(startDate, timeOffset.startDate); - const formattedEndOffset = isDateOnly ? '' : formatTimezoneOffset(endDate, timeOffset.endDate); - return startDate + formattedStartOffset + ' ' + '—' + ' ' + endDate + formattedEndOffset; + return ( + formatDate({ + date: startDate, + format, + hideTimeOffset, + isDateOnly, + timeOffset: timeOffset.startDate, + locale, + }) + + ' — ' + + formatDate({ + date: endDate, + format, + hideTimeOffset, + isDateOnly, + timeOffset: timeOffset.endDate, + locale, + }) + ); +} + +function formatDate({ + date, + format, + hideTimeOffset, + isDateOnly, + timeOffset, + locale, +}: { + date: string; + format: DateRangePickerProps.AbsoluteFormat; + hideTimeOffset?: boolean; + isDateOnly: boolean; + timeOffset?: number; + locale?: string; +}) { + switch (format) { + case 'long-localized': { + return formatDateLocalized({ date, hideTimeOffset, isDateOnly, locale, timeOffset }); + } + default: { + return formatDateIso({ date, hideTimeOffset, isDateOnly, timeOffset }); + } + } } diff --git a/src/internal/utils/date-time/format-time-offset.ts b/src/internal/utils/date-time/format-time-offset.ts new file mode 100644 index 0000000000..96cbe517e3 --- /dev/null +++ b/src/internal/utils/date-time/format-time-offset.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { padLeftZeros } from '../strings'; + +export function formatTimeOffsetISO(isoDate: string, offsetInMinutes?: number) { + offsetInMinutes = defaultToLocal(isoDate, offsetInMinutes); + const { hours, minutes } = getMinutesAndHours(offsetInMinutes); + + const sign = offsetInMinutes < 0 ? '-' : '+'; + const formattedOffset = `${sign}${formatISO2Digits(hours)}:${formatISO2Digits(minutes)}`; + + return formattedOffset; +} + +export function formatTimeOffsetLocalized(isoDate: string, offsetInMinutes?: number) { + offsetInMinutes = defaultToLocal(isoDate, offsetInMinutes); + if (offsetInMinutes === 0) { + return '(UTC)'; + } + const { hours, minutes } = getMinutesAndHours(offsetInMinutes); + + const sign = offsetInMinutes < 0 ? '-' : '+'; + const formattedMinutes = minutes === 0 ? '' : `:${minutes}`; + const formattedOffset = `(UTC${sign}${hours}${formattedMinutes})`; + + return formattedOffset; +} + +function defaultToLocal(isoDate: string, offsetInMinutes?: number) { + return offsetInMinutes ?? 0 - new Date(isoDate).getTimezoneOffset(); +} + +function getMinutesAndHours(minutes: number) { + return { hours: Math.floor(Math.abs(minutes) / 60), minutes: Math.abs(minutes % 60) }; +} + +function formatISO2Digits(n: number) { + return padLeftZeros(n.toFixed(0), 2); +} diff --git a/src/internal/utils/date-time/format-timezone-offset.ts b/src/internal/utils/date-time/format-timezone-offset.ts deleted file mode 100644 index 82065e49b7..0000000000 --- a/src/internal/utils/date-time/format-timezone-offset.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { padLeftZeros } from '../strings'; - -export function formatTimezoneOffset(isoDate: string, offsetInMinutes?: number) { - offsetInMinutes = offsetInMinutes ?? 0 - new Date(isoDate).getTimezoneOffset(); - const hoursOffset = padLeftZeros(Math.floor(Math.abs(offsetInMinutes) / 60).toFixed(0), 2); - const minuteOffset = padLeftZeros(Math.abs(offsetInMinutes % 60).toFixed(0), 2); - - const sign = offsetInMinutes < 0 ? '-' : '+'; - const formattedOffset = `${sign}${hoursOffset}:${minuteOffset}`; - - return formattedOffset; -} diff --git a/src/internal/utils/date-time/index.ts b/src/internal/utils/date-time/index.ts index 1662f2a7ec..2d121b2356 100644 --- a/src/internal/utils/date-time/index.ts +++ b/src/internal/utils/date-time/index.ts @@ -6,7 +6,7 @@ export { formatDateRange } from './format-date-range'; export { formatDate } from './format-date'; export { formatTime } from './format-time'; export { formatDateTime } from './format-date-time'; -export { formatTimezoneOffset } from './format-timezone-offset'; +export { formatTimeOffsetISO } from './format-time-offset'; export { isIsoDateOnly } from './is-iso-date-only'; export { joinDateTime, splitDateTime } from './join-date-time'; export { parseDate } from './parse-date';