Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new function roundToNearestHours #2752

Merged
merged 2 commits into from
Mar 11, 2024
Merged
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
86 changes: 86 additions & 0 deletions src/roundToNearestHours/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { getRoundingMethod } from "../_lib/getRoundingMethod/index.js";
import { constructFrom } from "../constructFrom/index.js";
import { toDate } from "../toDate/index.js";
import type {
NearestHours,
NearestToUnitOptions,
RoundingOptions,
} from "../types.js";

/**
* The {@link roundToNearestHours} function options.
*/
export interface RoundToNearestHoursOptions
extends NearestToUnitOptions<NearestHours>,
RoundingOptions {}

/**
* @name roundToNearestHours
* @category Hour Helpers
* @summary Rounds the given date to the nearest hour
*
* @description
* Rounds the given date to the nearest hour (or number of hours).
* Rounds up when the given date is exactly between the nearest round hours.
*
* @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
*
* @param date - The date to round
* @param options - An object with options.
*
* @returns The new date rounded to the closest hour
*
* @example
* // Round 10 July 2014 12:34:56 to nearest hour:
* const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56))
* //=> Thu Jul 10 2014 13:00:00
*
* @example
* // Round 10 July 2014 12:34:56 to nearest half hour:
* const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56), { nearestTo: 6 })
* //=> Thu Jul 10 2014 12:00:00

* @example
* // Round 10 July 2014 12:34:56 to nearest half hour:
* const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56), { nearestTo: 8 })
* //=> Thu Jul 10 2014 16:00:00

* @example
* // Floor (rounds down) 10 July 2014 12:34:56 to nearest hour:
* const result = roundToNearestHours(new Date(2014, 6, 10, 1, 23, 45), { roundingMethod: 'ceil' })
* //=> Thu Jul 10 2014 02:00:00
*
* @example
* // Ceil (rounds up) 10 July 2014 12:34:56 to nearest quarter hour:
* const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56), { roundingMethod: 'floor', nearestTo: 8 })
* //=> Thu Jul 10 2014 08:00:00
*/
export function roundToNearestHours<DateType extends Date>(
date: DateType | number | string,
options?: RoundToNearestHoursOptions,
): Date {
const nearestTo = options?.nearestTo ?? 1;

if (nearestTo < 1 || nearestTo > 12) return constructFrom(date, NaN);

const _date = toDate(date);
const fractionalMinutes = _date.getMinutes() / 60;
const fractionalSeconds = _date.getSeconds() / 60 / 60;
const fractionalMilliseconds = _date.getMilliseconds() / 1000 / 60 / 60;
const hours =
_date.getHours() +
fractionalMinutes +
fractionalSeconds +
fractionalMilliseconds;

// Unlike the `differenceIn*` functions, the default rounding behavior is `round` and not 'trunc'
const method = options?.roundingMethod ?? "round";
const roundingMethod = getRoundingMethod(method);

// nearestTo option does not care daylight savings time
const roundedHours = roundingMethod(hours / nearestTo) * nearestTo;

const result = constructFrom(date, _date);
result.setHours(roundedHours, 0, 0, 0);
return result;
}
301 changes: 301 additions & 0 deletions src/roundToNearestHours/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
/* eslint-env mocha */

import assert from "node:assert";
import { describe, it } from "vitest";
import {
roundToNearestHours,
type RoundToNearestHoursOptions,
} from "./index.js";

describe("roundToNearestHours", () => {
it("rounds given date to the nearest hour by default", () => {
// low
assert.deepStrictEqual(roundToNearestHours(makeDate(15, 10)), makeDate(15));

// mid-point
assert.deepStrictEqual(roundToNearestHours(makeDate(15, 30)), makeDate(16));

// high
assert.deepStrictEqual(roundToNearestHours(makeDate(15, 59)), makeDate(16));
});

it("rounds to the closest x hours if nearestTo is provided", () => {
const options: RoundToNearestHoursOptions = { nearestTo: 3 };

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(9, 1), options),
makeDate(9),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(10, 30), options),
makeDate(12),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(11, 59), options),
makeDate(12),
);
});

describe("roundingMethod", () => {
it("trunc, nearestTo === 1 (default)", () => {
const options: RoundToNearestHoursOptions = { roundingMethod: "trunc" };

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 10), options),
makeDate(15),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 30), options),
makeDate(15),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 59), options),
makeDate(15),
);
});

it("trunc, nearestTo === 3", () => {
const options: RoundToNearestHoursOptions = {
roundingMethod: "trunc",
nearestTo: 3,
};

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(9), options),
makeDate(9),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(10, 30), options),
makeDate(9),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(11, 59), options),
makeDate(9),
);
});

it("floor, nearestTo === 1 (default)", () => {
const options: RoundToNearestHoursOptions = { roundingMethod: "floor" };

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15), options),
makeDate(15),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 30), options),
makeDate(15),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 59), options),
makeDate(15),
);
});

it("floor, nearestTo === 3", () => {
const options: RoundToNearestHoursOptions = {
roundingMethod: "floor",
nearestTo: 3,
};

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15), options),
makeDate(15),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(16, 30), options),
makeDate(15),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(17, 59), options),
makeDate(15),
);
});

it("ceil, nearestTo === 1 (default)", () => {
const options: RoundToNearestHoursOptions = { roundingMethod: "ceil" };

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 1), options),
makeDate(16),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 30), options),
makeDate(16),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 59), options),
makeDate(16),
);
});

it("ceil, nearestTo === 3", () => {
const options: RoundToNearestHoursOptions = {
roundingMethod: "ceil",
nearestTo: 3,
};

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 1), options),
makeDate(18),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(16, 30), options),
makeDate(18),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(17, 59), options),
makeDate(18),
);
});

it("round, nearestTo === 1 (default)", () => {
const options: RoundToNearestHoursOptions = { roundingMethod: "round" };

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15), options),
makeDate(15),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 30), options),
makeDate(16),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 59), options),
makeDate(16),
);
});

it("round, nearestTo === 3", () => {
const options: RoundToNearestHoursOptions = {
roundingMethod: "round",
nearestTo: 3,
};

// low
assert.deepStrictEqual(
roundToNearestHours(makeDate(15), options),
makeDate(15),
);

// mid-point
assert.deepStrictEqual(
roundToNearestHours(makeDate(16, 30), options),
makeDate(18),
);

// high
assert.deepStrictEqual(
roundToNearestHours(makeDate(17, 59), options),
makeDate(18),
);
});
});

describe("edge cases", () => {
it("rounds up to the next day", () => {
assert.deepStrictEqual(
roundToNearestHours(new Date(2014, 6, 10, 23, 59, 59, 999)),
new Date(2014, 6, 11),
);
});

it("ceils correctly with 0 seconds and 1 millisecond", () => {
// "ceil" does not round up when exactly oclock
assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 0, 0, 0), { roundingMethod: "ceil" }),
makeDate(15),
);

assert.deepStrictEqual(
roundToNearestHours(makeDate(15, 0, 0, 1), { roundingMethod: "ceil" }),
makeDate(16),
);
});
});

describe("examples", () => {
it("example 1", () => {
const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56));
assert.deepStrictEqual(result, new Date(2014, 6, 10, 13));
});

it("example 2", () => {
const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56), {
nearestTo: 6,
});
assert.deepStrictEqual(result, new Date(2014, 6, 10, 12));
});

it("example 3", () => {
const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56), {
nearestTo: 8,
});
assert.deepStrictEqual(result, new Date(2014, 6, 10, 16));
});

it("example 4", () => {
const result = roundToNearestHours(new Date(2014, 6, 10, 1, 23, 45), {
roundingMethod: "ceil",
});
assert.deepStrictEqual(result, new Date(2014, 6, 10, 2));
});

it("example 5", () => {
const result = roundToNearestHours(new Date(2014, 6, 10, 12, 34, 56), {
roundingMethod: "floor",
nearestTo: 8,
});
assert.deepStrictEqual(result, new Date(2014, 6, 10, 8));
});
});
});

function makeDate(
hours: number,
minutes: number = 0,
seconds: number = 0,
milliseconds: number = 0,
) {
// helper to make tests more readable since we mostly care about hours and minutes
return new Date(2014, 6 /* Jul */, 10, hours, minutes, seconds, milliseconds);
}
Loading
Loading