diff --git a/src/weather/services/weather.ts b/src/weather/services/weather.ts index ca8f4bb..9bfdf31 100644 --- a/src/weather/services/weather.ts +++ b/src/weather/services/weather.ts @@ -7,7 +7,7 @@ import type { Weather, WeatherResponse, } from "../interfaces"; -import { formatMilliseconds } from "../utils/time-formatter"; +import { formatMilliseconds } from "../utils/time"; const FORECAST_MAX_HOURS = 10; const UNIT_SYSTEM = "metric"; diff --git a/src/weather/utils/__tests__/data/index.ts b/src/weather/utils/__tests__/data/index.ts new file mode 100644 index 0000000..0a6eac2 --- /dev/null +++ b/src/weather/utils/__tests__/data/index.ts @@ -0,0 +1,95 @@ +const MINUTE = 60000; +const HOUR = MINUTE * 60; + +export const ES_LOCALE_TEST_SUITE = [ + [0, "00:00"], + [MINUTE, "00:01"], + [2 * MINUTE, "00:02"], + [HOUR, "01:00"], + [2 * HOUR, "02:00"], + [12 * HOUR, "12:00"], + [13 * HOUR, "13:00"], + [23 * HOUR, "23:00"], + [24 * HOUR, "00:00"], +] as const; +export const EN_LOCALE_TEST_SUITE = [ + [0, "12:00 AM"], + [MINUTE, "12:01 AM"], + [2 * MINUTE, "12:02 AM"], + [HOUR, "01:00 AM"], + [2 * HOUR, "02:00 AM"], + [12 * HOUR, "12:00 PM"], + [13 * HOUR, "01:00 PM"], + [23 * HOUR, "11:00 PM"], + [24 * HOUR, "12:00 AM"], +] as const; + +export const IS_DAY_TEST_SUITE = [ + { + time: new Date(Date.UTC(2022, 1, 1, 0, 0, 0)), // 00:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 12, 0, 0)), // 12:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: true, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 23, 59, 59)), // 23:59 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + { + time: new Date(Date.UTC(2022, 1, 2, 0, 0, 0)), // 00:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 5, 59, 59)), // 05:59 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 18, 0, 1)), // 18:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: true, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 5, 0, 0)), // 05:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + { + time: new Date(Date.UTC(2022, 1, 1, 19, 0, 0)), // 19:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, + // Different year dates + { + time: new Date(Date.UTC(2023, 1, 1, 12, 0, 0)), // 12:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: true, + }, + { + time: new Date(Date.UTC(2023, 1, 1, 0, 0, 0)), // 0:00 + sunrise: new Date(Date.UTC(2022, 1, 1, 6, 0, 0)), // 06:00 + sunset: new Date(Date.UTC(2022, 1, 1, 18, 0, 0)), // 18:00 + result: false, + }, +] as const; diff --git a/src/weather/utils/__tests__/time-formatter.spec.ts b/src/weather/utils/__tests__/time-formatter.spec.ts deleted file mode 100644 index e61035d..0000000 --- a/src/weather/utils/__tests__/time-formatter.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { formatMilliseconds } from "../time-formatter"; - -const MINUTE = 60000; -const HOUR = MINUTE * 60; - -const ES_LOCALE_TEST_SUITE = [ - [0, "00:00"], - [MINUTE, "00:01"], - [2 * MINUTE, "00:02"], - [HOUR, "01:00"], - [2 * HOUR, "02:00"], - [12 * HOUR, "12:00"], - [13 * HOUR, "13:00"], - [23 * HOUR, "23:00"], - [24 * HOUR, "00:00"], -] as const; -const EN_LOCALE_TEST_SUITE = [ - [0, "12:00 AM"], - [MINUTE, "12:01 AM"], - [2 * MINUTE, "12:02 AM"], - [HOUR, "01:00 AM"], - [2 * HOUR, "02:00 AM"], - [12 * HOUR, "12:00 PM"], - [13 * HOUR, "01:00 PM"], - [23 * HOUR, "11:00 PM"], - [24 * HOUR, "12:00 AM"], -] as const; - -describe("Weather Service Time formatter", () => { - const esLocale = "es-ES"; - it.each(ES_LOCALE_TEST_SUITE)( - `should format time for ${esLocale}`, - (time, result) => { - expect(formatMilliseconds({ time, locale: esLocale })).toBe(result); - } - ); - - const enLocale = "en-US"; - it.each(EN_LOCALE_TEST_SUITE)( - `should format time for ${enLocale}`, - (time, result) => { - expect(formatMilliseconds({ time, locale: enLocale })).toBe(result); - } - ); -}); diff --git a/src/weather/utils/__tests__/time.spec.ts b/src/weather/utils/__tests__/time.spec.ts new file mode 100644 index 0000000..e05e73a --- /dev/null +++ b/src/weather/utils/__tests__/time.spec.ts @@ -0,0 +1,40 @@ +import { formatMilliseconds, isDayTime } from "../time"; +import { + EN_LOCALE_TEST_SUITE, + ES_LOCALE_TEST_SUITE, + IS_DAY_TEST_SUITE, +} from "./data"; + +describe("Weather Service Time formatter", () => { + const esLocale = "es-ES"; + it.each(ES_LOCALE_TEST_SUITE)( + `should format time for ${esLocale}`, + (time, result) => { + expect(formatMilliseconds({ time, locale: esLocale })).toBe(result); + } + ); + + const enLocale = "en-US"; + it.each(EN_LOCALE_TEST_SUITE)( + `should format time for ${enLocale}`, + (time, result) => { + expect(formatMilliseconds({ time, locale: enLocale })).toBe(result); + } + ); + + IS_DAY_TEST_SUITE.forEach(({ time, sunrise, sunset, result }) => { + it(`should correctly determine if it is day time for ${formatMilliseconds({ + time: time.getTime(), + locale: enLocale, + timeZone: "UTC", + })}`, () => { + expect( + isDayTime({ + time, + sunrise, + sunset, + }) + ).toBe(result); + }); + }); +}); diff --git a/src/weather/utils/time-formatter.ts b/src/weather/utils/time-formatter.ts deleted file mode 100644 index 5cc4938..0000000 --- a/src/weather/utils/time-formatter.ts +++ /dev/null @@ -1,18 +0,0 @@ -interface formatMillisecondsOptions { - time: number; - locale: string; - timeZone?: string; -} - -export const formatMilliseconds = ({ time, locale, timeZone = 'UTC' }: formatMillisecondsOptions) => { - const timeFormatter = new Intl.DateTimeFormat( - locale, - { - hour: '2-digit', - minute: '2-digit', - timeZone - } - ); - - return timeFormatter.format(new Date(time)); -} \ No newline at end of file diff --git a/src/weather/utils/time.ts b/src/weather/utils/time.ts new file mode 100644 index 0000000..6cf2735 --- /dev/null +++ b/src/weather/utils/time.ts @@ -0,0 +1,70 @@ +interface formatMillisecondsOptions { + time: number; + locale: string; + timeZone?: string; +} + +/** + * Formats a given time in milliseconds to a localized time string. + * + * @param {Object} options - The options object. + * @param {number} options.time - The time in milliseconds since the Unix epoch (January 1, 1970 00:00:00 UTC). + * @param {string} options.locale - A string with a BCP 47 language tag, or an array of such strings, to specify the locale. + * @param {string} [options.timeZone="UTC"] - An optional string representing the time zone to use. Defaults to "UTC". + * @returns {string} - A string representing the formatted time according to the specified locale and time zone. + * + * @example + * // Example usage: + * const formattedTime = formatMilliseconds({ time: Date.now(), locale: 'en-US', timeZone: 'America/New_York' }); + * console.log(formattedTime); // Output: "02:30 PM" (depending on the current time) + */ +export const formatMilliseconds = ({ + time, + locale, + timeZone = "UTC", +}: formatMillisecondsOptions) => { + const timeFormatter = new Intl.DateTimeFormat(locale, { + hour: "2-digit", + minute: "2-digit", + timeZone, + }); + + return timeFormatter.format(new Date(time)); +}; + +interface IsDayTimeProps { + time: Date; + sunrise: Date; + sunset: Date; +} + +/** + * Determines whether a given time is during the day, based on sunrise and sunset times, + * ignoring the date component and only considering the hours and minutes. + * + * @param {Object} params - The parameters object. + * @param {Date} params.time - The current time to check. + * @param {Date} params.sunrise - The time of sunrise. + * @param {Date} params.sunset - The time of sunset. + * @returns {boolean} - Returns `true` if the time is between sunrise and sunset, otherwise `false`. + */ +export const isDayTime = ({ + time, + sunrise, + sunset, +}: IsDayTimeProps): boolean => { + const getTimeInMilliseconds = (date: Date) => + date.getUTCHours() * 60 * 60 + + date.getUTCMinutes() * 60 + + date.getUTCSeconds() * 1000 + + date.getUTCMilliseconds(); + + const timeInMilliseconds = getTimeInMilliseconds(time); + const sunriseInMilliseconds = getTimeInMilliseconds(sunrise); + const sunsetInMilliseconds = getTimeInMilliseconds(sunset); + + return ( + timeInMilliseconds >= sunriseInMilliseconds && + timeInMilliseconds <= sunsetInMilliseconds + ); +};