From affba804546607c0d26887bb5c40cba9c9be3fe6 Mon Sep 17 00:00:00 2001 From: Ali Hasani Date: Sat, 27 Jun 2020 15:40:45 +0430 Subject: [PATCH] feat(std/datetime): add is leap and difference functions (#4857) --- std/datetime/mod.ts | 134 +++++++++++++++++++++++++++- std/datetime/test.ts | 202 +++++++++++++++++++++++++++---------------- 2 files changed, 259 insertions(+), 77 deletions(-) diff --git a/std/datetime/mod.ts b/std/datetime/mod.ts index 7503eccb43..99332e0467 100644 --- a/std/datetime/mod.ts +++ b/std/datetime/mod.ts @@ -3,6 +3,12 @@ import { assert } from "../_util/assert.ts"; export type DateFormat = "mm-dd-yyyy" | "dd-mm-yyyy" | "yyyy-mm-dd"; +export const SECOND = 1e3; +export const MINUTE = SECOND * 60; +export const HOUR = MINUTE * 60; +export const DAY = HOUR * 24; +export const WEEK = DAY * 7; + function execForce(reg: RegExp, pat: string): RegExpExecArray { const v = reg.exec(pat); assert(v != null); @@ -96,13 +102,12 @@ export function parseDateTime( * @return Number of the day in year */ export function dayOfYear(date: Date): number { - const dayMs = 1000 * 60 * 60 * 24; const yearStart = new Date(date.getFullYear(), 0, 0); const diff = date.getTime() - yearStart.getTime() + (yearStart.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000; - return Math.floor(diff / dayMs); + return Math.floor(diff / DAY); } /** @@ -150,3 +155,128 @@ export function toIMF(date: Date): string { months[date.getUTCMonth()] } ${y} ${h}:${min}:${s} GMT`; } + +/** + * Check given year is a leap year or not. + * based on : https://docs.microsoft.com/en-us/office/troubleshoot/excel/determine-a-leap-year + * @param year year in number or Date format + */ +export function isLeap(year: Date | number): boolean { + const yearNumber = year instanceof Date ? year.getFullYear() : year; + return ( + (yearNumber % 4 === 0 && yearNumber % 100 !== 0) || yearNumber % 400 === 0 + ); +} + +export type Unit = + | "miliseconds" + | "seconds" + | "minutes" + | "hours" + | "days" + | "weeks" + | "months" + | "quarters" + | "years"; + +export type DifferenceFormat = Partial>; + +export type DifferenceOptions = { + units?: Unit[]; +}; + +/** + * Calculate difference between two dates. + * @param from Year to calculate difference + * @param to Year to calculate difference with + * @param options Options for determining how to respond + * + * example : + * + * ```typescript + * datetime.difference(new Date("2020/1/1"),new Date("2020/2/2"),{ units : ["days","months"] }) + * ``` + */ +export function difference( + from: Date, + to: Date, + options?: DifferenceOptions +): DifferenceFormat { + const uniqueUnits = options?.units + ? [...new Set(options?.units)] + : [ + "miliseconds", + "seconds", + "minutes", + "hours", + "days", + "weeks", + "months", + "quarters", + "years", + ]; + + const bigger = Math.max(from.getTime(), to.getTime()); + const smaller = Math.min(from.getTime(), to.getTime()); + const differenceInMs = bigger - smaller; + + const differences: DifferenceFormat = {}; + + for (const uniqueUnit of uniqueUnits) { + switch (uniqueUnit) { + case "miliseconds": + differences.miliseconds = differenceInMs; + break; + case "seconds": + differences.seconds = Math.floor(differenceInMs / SECOND); + break; + case "minutes": + differences.minutes = Math.floor(differenceInMs / MINUTE); + break; + case "hours": + differences.hours = Math.floor(differenceInMs / HOUR); + break; + case "days": + differences.days = Math.floor(differenceInMs / DAY); + break; + case "weeks": + differences.weeks = Math.floor(differenceInMs / WEEK); + break; + case "months": + differences.months = calculateMonthsDifference(bigger, smaller); + break; + case "quarters": + differences.quarters = Math.floor( + (typeof differences.months !== "undefined" && + differences.months / 4) || + calculateMonthsDifference(bigger, smaller) / 4 + ); + break; + case "years": + differences.years = Math.floor( + (typeof differences.months !== "undefined" && + differences.months / 12) || + calculateMonthsDifference(bigger, smaller) / 12 + ); + break; + } + } + + return differences; +} + +function calculateMonthsDifference(bigger: number, smaller: number): number { + const biggerDate = new Date(bigger); + const smallerDate = new Date(smaller); + const yearsDiff = biggerDate.getFullYear() - smallerDate.getFullYear(); + const monthsDiff = biggerDate.getMonth() - smallerDate.getMonth(); + const calendarDiffrences = Math.abs(yearsDiff * 12 + monthsDiff); + const compareResult = biggerDate > smallerDate ? 1 : -1; + biggerDate.setMonth( + biggerDate.getMonth() - compareResult * calendarDiffrences + ); + const isLastMonthNotFull = + biggerDate > smallerDate ? 1 : -1 === -compareResult ? 1 : 0; + const months = compareResult * (calendarDiffrences - isLastMonthNotFull); + return months === 0 ? 0 : months; +} diff --git a/std/datetime/test.ts b/std/datetime/test.ts index c509143422..dc4c7278e5 100644 --- a/std/datetime/test.ts +++ b/std/datetime/test.ts @@ -1,83 +1,101 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { assertEquals, assertThrows } from "../testing/asserts.ts"; +import { assert, assertEquals, assertThrows } from "../testing/asserts.ts"; import * as datetime from "./mod.ts"; -Deno.test("parseDateTime", function (): void { - assertEquals( - datetime.parseDateTime("01-03-2019 16:30", "mm-dd-yyyy hh:mm"), - new Date(2019, 0, 3, 16, 30) - ); - assertEquals( - datetime.parseDateTime("03-01-2019 16:31", "dd-mm-yyyy hh:mm"), - new Date(2019, 0, 3, 16, 31) - ); - assertEquals( - datetime.parseDateTime("2019-01-03 16:32", "yyyy-mm-dd hh:mm"), - new Date(2019, 0, 3, 16, 32) - ); - assertEquals( - datetime.parseDateTime("16:33 01-03-2019", "hh:mm mm-dd-yyyy"), - new Date(2019, 0, 3, 16, 33) - ); - assertEquals( - datetime.parseDateTime("16:34 03-01-2019", "hh:mm dd-mm-yyyy"), - new Date(2019, 0, 3, 16, 34) - ); - assertEquals( - datetime.parseDateTime("16:35 2019-01-03", "hh:mm yyyy-mm-dd"), - new Date(2019, 0, 3, 16, 35) - ); -}); - -Deno.test("invalidParseDateTimeFormatThrows", function (): void { - assertThrows( - (): void => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (datetime as any).parseDateTime("2019-01-01 00:00", "x-y-z"); - }, - Error, - "Invalid datetime format!" - ); -}); - -Deno.test("parseDate", function (): void { - assertEquals( - datetime.parseDate("01-03-2019", "mm-dd-yyyy"), - new Date(2019, 0, 3) - ); - assertEquals( - datetime.parseDate("03-01-2019", "dd-mm-yyyy"), - new Date(2019, 0, 3) - ); - assertEquals( - datetime.parseDate("2019-01-03", "yyyy-mm-dd"), - new Date(2019, 0, 3) - ); -}); - -Deno.test("invalidParseDateFormatThrows", function (): void { - assertThrows( - (): void => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (datetime as any).parseDate("2019-01-01", "x-y-z"); - }, - Error, - "Invalid date format!" - ); -}); - -Deno.test("DayOfYear", function (): void { - assertEquals(1, datetime.dayOfYear(new Date("2019-01-01T03:24:00"))); - assertEquals(70, datetime.dayOfYear(new Date("2019-03-11T03:24:00"))); - assertEquals(365, datetime.dayOfYear(new Date("2019-12-31T03:24:00"))); -}); - -Deno.test("currentDayOfYear", function (): void { - assertEquals(datetime.currentDayOfYear(), datetime.dayOfYear(new Date())); +Deno.test({ + name: "[std/datetime] parseDate", + fn: () => { + assertEquals( + datetime.parseDateTime("01-03-2019 16:30", "mm-dd-yyyy hh:mm"), + new Date(2019, 0, 3, 16, 30) + ); + assertEquals( + datetime.parseDateTime("03-01-2019 16:31", "dd-mm-yyyy hh:mm"), + new Date(2019, 0, 3, 16, 31) + ); + assertEquals( + datetime.parseDateTime("2019-01-03 16:32", "yyyy-mm-dd hh:mm"), + new Date(2019, 0, 3, 16, 32) + ); + assertEquals( + datetime.parseDateTime("16:33 01-03-2019", "hh:mm mm-dd-yyyy"), + new Date(2019, 0, 3, 16, 33) + ); + assertEquals( + datetime.parseDateTime("16:34 03-01-2019", "hh:mm dd-mm-yyyy"), + new Date(2019, 0, 3, 16, 34) + ); + assertEquals( + datetime.parseDateTime("16:35 2019-01-03", "hh:mm yyyy-mm-dd"), + new Date(2019, 0, 3, 16, 35) + ); + }, }); Deno.test({ - name: "[DateTime] to IMF", + name: "[std/datetime] invalidParseDateTimeFormatThrows", + fn: () => { + assertThrows( + (): void => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (datetime as any).parseDateTime("2019-01-01 00:00", "x-y-z"); + }, + Error, + "Invalid datetime format!" + ); + }, +}); + +Deno.test({ + name: "[std/datetime] parseDate", + fn: () => { + assertEquals( + datetime.parseDate("01-03-2019", "mm-dd-yyyy"), + new Date(2019, 0, 3) + ); + assertEquals( + datetime.parseDate("03-01-2019", "dd-mm-yyyy"), + new Date(2019, 0, 3) + ); + assertEquals( + datetime.parseDate("2019-01-03", "yyyy-mm-dd"), + new Date(2019, 0, 3) + ); + }, +}); + +Deno.test({ + name: "[std/datetime] invalidParseDateFormatThrows", + fn: () => { + assertThrows( + (): void => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (datetime as any).parseDate("2019-01-01", "x-y-z"); + }, + Error, + "Invalid date format!" + ); + }, +}); + +Deno.test({ + name: "[std/datetime] DayOfYear", + fn: () => { + assertEquals(1, datetime.dayOfYear(new Date("2019-01-01T03:24:00"))); + assertEquals(70, datetime.dayOfYear(new Date("2019-03-11T03:24:00"))); + assertEquals(365, datetime.dayOfYear(new Date("2019-12-31T03:24:00"))); + }, +}); + +Deno.test({ + name: "[std/datetime] currentDayOfYear", + fn: () => { + assertEquals(datetime.currentDayOfYear(), datetime.dayOfYear(new Date())); + }, +}); + +Deno.test({ + name: "[std/datetime] to IMF", fn(): void { const actual = datetime.toIMF(new Date(Date.UTC(1994, 3, 5, 15, 32))); const expected = "Tue, 05 Apr 1994 15:32:00 GMT"; @@ -86,10 +104,44 @@ Deno.test({ }); Deno.test({ - name: "[DateTime] to IMF 0", + name: "[std/datetime] to IMF 0", fn(): void { const actual = datetime.toIMF(new Date(0)); const expected = "Thu, 01 Jan 1970 00:00:00 GMT"; assertEquals(actual, expected); }, }); + +Deno.test({ + name: "[std/datetime] isLeap", + fn(): void { + assert(datetime.isLeap(1992)); + assert(datetime.isLeap(2000)); + assert(!datetime.isLeap(2003)); + assert(!datetime.isLeap(2007)); + }, +}); + +Deno.test({ + name: "[std/datetime] Difference", + fn(): void { + const denoInit = new Date("2018/5/14"); + const denoRelaseV1 = new Date("2020/5/13"); + let difference = datetime.difference(denoRelaseV1, denoInit, { + units: ["days", "months", "years"], + }); + assertEquals(difference.days, 730); + assertEquals(difference.months, 23); + assertEquals(difference.years, 1); + + const birth = new Date("1998/2/23 10:10:10"); + const old = new Date("1998/2/23 11:11:11"); + difference = datetime.difference(birth, old, { + units: ["miliseconds", "minutes", "seconds", "hours"], + }); + assertEquals(difference.miliseconds, 3661000); + assertEquals(difference.seconds, 3661); + assertEquals(difference.minutes, 61); + assertEquals(difference.hours, 1); + }, +});