Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/date-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { isOnOrBefore } from './isOnOrBefore';
export { isSameTZDay } from './isSameTZDay';
export { isSameTZMonth } from './isSameTZMonth';
export { isSameUTCDay } from './isSameUTCDay';
export { isSameUTCDayAndTime } from './isSameUTCDayAndTime';
export { isSameUTCMonth } from './isSameUTCMonth';
export { isSameUTCRange } from './isSameUTCRange';
export { isTodayTZ } from './isTodayTZ';
Expand All @@ -31,6 +32,7 @@ export { maxDate } from './maxDate';
export { minDate } from './minDate';
export { newTZDate } from './newTZDate';
export { newUTC } from './newUTC';
export { newUTCFromTimeZone } from './newUTCFromTimeZone';
export { setToUTCMidnight } from './setToUTCMidnight';
export { setUTCDate } from './setUTCDate';
export { setUTCMonth } from './setUTCMonth';
Expand Down
1 change: 1 addition & 0 deletions packages/date-utils/src/isSameUTCDayAndTime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { isSameUTCDayAndTime } from './isSameUTCDayAndTime';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { isSameUTCDayAndTime } from './isSameUTCDayAndTime';

describe('packages/time-input/utils/isSameUTCDayAndTime', () => {
test('returns true if the two dates are the same day and time in UTC', () => {
const date1 = new Date('2025-01-01T12:00:00Z');
const date2 = new Date('2025-01-01T12:00:00Z');
expect(isSameUTCDayAndTime(date1, date2)).toBe(true);
});

test('returns false if the two dates are not the same day in UTC', () => {
const date1 = new Date('2025-01-01T12:00:00Z');
const date2 = new Date('2025-01-02T12:00:00Z');
expect(isSameUTCDayAndTime(date1, date2)).toBe(false);
});

test('returns false if the two dates are not the same time in UTC', () => {
const date1 = new Date('2025-01-01T12:00:00Z');
const date2 = new Date('2025-01-01T12:00:01Z');
expect(isSameUTCDayAndTime(date1, date2)).toBe(false);
});

test('returns false if the two dates are not the same date and time in UTC', () => {
const date1 = new Date('2025-02-01T12:00:00Z');
const date2 = new Date('2025-01-01T12:00:01Z');
expect(isSameUTCDayAndTime(date1, date2)).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isValidDate } from '../isValidDate';
import { DateType } from '../types';

/**
* Checks if two dates are the same day and time in UTC.
*
* @param day1 - The first date to check
* @param day2 - The second date to check
* @returns Whether the two dates are the same day and time in UTC
*/
export const isSameUTCDayAndTime = (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably use isEqual from date-fns
https://date-fns.org/v4.1.0/docs/isEqual

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we need to explicitly check UTC, can we rename this isEqualUTC and move to date-utils?

day1?: DateType,
day2?: DateType,
): boolean => {
if (!isValidDate(day1) || !isValidDate(day2)) return false;

return (
day1.getUTCDate() === day2.getUTCDate() &&
day1.getUTCMonth() === day2.getUTCMonth() &&
day1.getUTCFullYear() === day2.getUTCFullYear() &&
day1.getUTCHours() === day2.getUTCHours() &&
day1.getUTCMinutes() === day2.getUTCMinutes() &&
day1.getUTCSeconds() === day2.getUTCSeconds()
);
};
1 change: 1 addition & 0 deletions packages/date-utils/src/newUTCFromTimeZone/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { newUTCFromTimeZone } from './newUTCFromTimeZone';
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { newUTCFromTimeZone } from './newUTCFromTimeZone';

describe('packages/date-utils/newUTCFromTimeZone', () => {
describe('UTC', () => {
test('creates a new UTC date from a given time zone', () => {
const date = newUTCFromTimeZone({
year: '2026',
month: '02',
day: '20',
hour: '23',
minute: '00',
second: '00',
timeZone: 'UTC',
});

// February 20, 2026 11:00:00 PM/23:00:00 in UTC is February 20, 2026 23:00:00 UTC
expect(date).toEqual(new Date('2026-02-20T23:00:00Z'));
});
});

describe('America/New_York', () => {
test('creates a new UTC date from a given time zone', () => {
const date = newUTCFromTimeZone({
year: '2026',
month: '02',
day: '20',
hour: '23',
minute: '00',
second: '00',
timeZone: 'America/New_York',
});

// February 20, 2026 11:00:00 PM/23:00:00 in America/New_York is February 21, 2026 04:00:00 UTC (UTC-5 hours)
expect(date).toEqual(new Date('2026-02-21T04:00:00Z'));
});
});

describe('Pacific/Kiritimati', () => {
test('creates a new UTC date from a given time zone', () => {
const date = newUTCFromTimeZone({
year: '2026',
month: '02',
day: '20',
hour: '23',
minute: '00',
second: '00',
timeZone: 'Pacific/Kiritimati',
});

// February 20, 2026 11:00:00 PM/23:00:00 in Pacific/Kiritimati is February 20, 2026 09:00:00 UTC (UTC+14 hours)
expect(date).toEqual(new Date('2026-02-20T09:00:00Z'));
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { zonedTimeToUtc } from 'date-fns-tz';

/**
* Creates a new UTC date from a given time zone.
* This takes the local date created above and converts it to UTC using the `zonedTimeToUtc` helper function.
*
* @param year - The year
* @param month - The month (1-12)
* @param day - The day
* @param hour - The hour in 24 hour format
* @param minute - The minute
* @param second - The second
* @param timeZone - The time zone
* @returns The new UTC date
*
* @example
* ```js
* // February 20, 2026 11:00:00 PM/23:00:00 in America/New_York is February 21, 2026 04:00:00 UTC
* newUTCFromTimeZone({ year: '2026', month: '02', day: '20', hour: '11', minute: '00', second: '00', timeZone: 'America/New_York' });
* // returns new Date('2026-02-21T04:00:00Z')
* ```
*/
export const newUTCFromTimeZone = ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this is doing the same thing as newTZDate

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're correct. I initially looked at this, and I thought it was doing something else.

You have this comment innewTZDate, // This API is less than perfect. Do you remember why you wrote that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function signature of newUTC matches the signature of new Date() (i.e. both support an array of numbers, newUTC(2025, 12, 19) etc)

Needing to prefix newTZDate with the offset feels a bit clunky to me
i.e. newTZDate(-5, 2025, 12, 19) just looks kinda wrong. Nothing inherently bad about it though

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your implementation supports IANA strings, so we could extend newTZDate to support that: newTZDate('America/New_York', 2025, 12, 19)

Copy link
Collaborator Author

@shaneeza shaneeza Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to pass the offset number and would rather use a string. I agree that it looks kinda wrong. I can update this to either:

With the IANA string and date, I can find the offset using getTimezoneOffset from date-fns, and continue to use what you have.

OR

With the IANA string and date, use the zonedTimeToUtc util from date-fns.

Regardless of the approach, we still need to depend on date-fns. I think that 2 would be easier because we don't need to calculate the offset manually.

year,
month,
day,
hour,
minute,
second,
timeZone,
}: {
year: string;
month: string;
day: string;
hour: string;
minute: string;
second: string;
timeZone: string;
}) => {
const newDate = new Date(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
Number(second),
);

return zonedTimeToUtc(newDate, timeZone);
};
4 changes: 2 additions & 2 deletions packages/time-input/src/TimeInputInputs/TimeInputInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { forwardRef } from 'react';
import { unitOptions } from '../constants';
import { useTimeInputContext, useTimeInputDisplayContext } from '../Context';
import { useSelectUnit } from '../hooks';
import { TimeSegmentsState } from '../shared.types';
import { DayPeriod, TimeSegmentsState } from '../shared.types';
import { TimeFormField, TimeFormFieldInputContainer } from '../TimeFormField';
import { TimeInputBox } from '../TimeInputBox/TimeInputBox';
import { TimeInputSelect } from '../TimeInputSelect/TimeInputSelect';
Expand Down Expand Up @@ -51,7 +51,7 @@ export const TimeInputInputs = forwardRef<HTMLDivElement, TimeInputInputsProps>(
* // TODO: This is temp and will be replaced in the next PR
*/
const { selectUnit, setSelectUnit } = useSelectUnit({
dayPeriod: timeParts.dayPeriod,
dayPeriod: timeParts.dayPeriod as DayPeriod,
value,
unitOptions,
});
Expand Down
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore this hook, it's being replaced in the next PR

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';

import { DateType, isValidDate } from '@leafygreen-ui/date-utils';

import { DayPeriod } from '../../shared.types';
import { UnitOption } from '../../TimeInputSelect/TimeInputSelect.types';

interface UseSelectUnitReturn {
Expand All @@ -17,7 +18,7 @@ interface UseSelectUnitReturn {
* @returns The select unit option.
*/
const findSelectUnit = (
dayPeriod: string,
dayPeriod: DayPeriod,
unitOptions: Array<UnitOption>,
): UnitOption => {
const selectUnitOption = unitOptions.find(
Expand All @@ -39,7 +40,7 @@ export const useSelectUnit = ({
value,
unitOptions,
}: {
dayPeriod: string;
dayPeriod: DayPeriod;
value: DateType | undefined;
unitOptions: Array<UnitOption>;
}): UseSelectUnitReturn => {
Expand Down
10 changes: 10 additions & 0 deletions packages/time-input/src/shared.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ export type TimeSegment = (typeof TimeSegment)[keyof typeof TimeSegment];

export type TimeSegmentsState = Record<TimeSegment, string>;

/*
* An enumerable object that maps the day period names to their values
*/
export const DayPeriod = {
AM: 'AM',
PM: 'PM',
} as const;

export type DayPeriod = (typeof DayPeriod)[keyof typeof DayPeriod];

/**
* The type for the time input segment change event
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import range from 'lodash/range';

import { consoleOnce } from '@leafygreen-ui/lib';

import { convert12hTo24h } from './convert12hTo24h';

describe('convert12hTo24h', () => {
describe('AM', () => {
test('12 AM converts to 0', () => {
expect(convert12hTo24h(12, 'AM')).toEqual(0);
});

test.each(range(1, 12).map(i => [i, i]))(
'%i AM converts to %i',
(input, expected) => {
expect(convert12hTo24h(input, 'AM')).toEqual(expected);
},
);
});

describe('PM', () => {
test('12 PM converts to 12', () => {
expect(convert12hTo24h(12, 'PM')).toEqual(12);
});

test.each(range(1, 12).map(i => [i, i + 12]))(
'%i PM converts to %i',
(input, expected) => {
expect(convert12hTo24h(input, 'PM')).toEqual(expected);
},
);
});

describe('Invalid hour', () => {
test('less than 1 returns the hour', () => {
const consoleWarnSpy = jest
.spyOn(consoleOnce, 'warn')
.mockImplementation(() => {});
expect(convert12hTo24h(0, 'AM')).toEqual(0);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'convert12hTo24h > Invalid hour: 0',
);
consoleWarnSpy.mockRestore();
});

test('greater than 12 returns the hour', () => {
const consoleWarnSpy = jest
.spyOn(consoleOnce, 'warn')
.mockImplementation(() => {});
expect(convert12hTo24h(13, 'AM')).toEqual(13);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'convert12hTo24h > Invalid hour: 13',
);
consoleWarnSpy.mockRestore();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { consoleOnce } from '@leafygreen-ui/lib';

import { DayPeriod } from '../../shared.types';

/**
* Converts a 12 hour format hour to a 24 hour format hour
*
* @param hour - The hour to convert
* @param dayPeriod - The day period to use for the conversion (AM or PM)
* @returns The converted hour or the original hour if it is invalid
*
* @example
* ```js
* convert12hTo24h(12, 'AM'); // 0
* convert12hTo24h(12, 'PM'); // 12
* convert12hTo24h(1, 'AM'); // 1
* convert12hTo24h(1, 'PM'); // 13
* convert12hTo24h(0, 'AM'); // 0
* convert12hTo24h(13, 'AM'); // 13
* ```
*/
export const convert12hTo24h = (hour: number, dayPeriod: DayPeriod): number => {
if (hour < 1 || hour > 12) {
consoleOnce.warn(`convert12hTo24h > Invalid hour: ${hour}`);
return hour;
}

if (hour === 12) {
// 12AM -> 0:00
// 12PM -> 12:00
return dayPeriod === DayPeriod.AM ? 0 : 12;
}

// if dayPeriod is PM, return hour + 12
if (dayPeriod === DayPeriod.PM) {
return hour + 12;
}

return hour;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { doesSomeSegmentExist } from './doesSomeSegmentExist';

describe('doesSomeSegmentExist', () => {
test('returns true if at least one segment is filled', () => {
expect(doesSomeSegmentExist({ hour: '', minute: '', second: '00' })).toBe(
true,
);
});

test('returns true if all segments are filled', () => {
expect(
doesSomeSegmentExist({ hour: '12', minute: '00', second: '00' }),
).toBe(true);
});

test('returns false if no segments are filled', () => {
expect(doesSomeSegmentExist({ hour: '', minute: '', second: '' })).toBe(
false,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { TimeSegmentsState } from '../../shared.types';

/**
* Checks if some segment exists
*
* @param segments - The segments to check
* @returns Whether some segment exists
*/
export const doesSomeSegmentExist = (segments: TimeSegmentsState) => {
// check if all segments are not empty
return Object.values(segments).some(segment => segment !== '');
};
Loading
Loading