Skip to content

Commit

Permalink
consistent intervals with interval.every
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Oct 29, 2023
1 parent 720eb23 commit 93c5637
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 158 deletions.
31 changes: 8 additions & 23 deletions src/day.js
@@ -1,35 +1,20 @@
import {timeInterval} from "./interval.js";
import {durationDay, durationMinute} from "./duration.js";
import {timeEpoch} from "./epoch.js";

export const timeDay = timeInterval(
date => date.setHours(0, 0, 0, 0),
(date) => date.setHours(0, 0, 0, 0),
(date, step) => date.setDate(date.getDate() + step),
(start, end) => (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationDay,
date => date.getDate() - 1
timeEpoch
);

export const timeDays = timeDay.range;

export const utcDay = timeInterval((date) => {
date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
date.setUTCDate(date.getUTCDate() + step);
}, (start, end) => {
return (end - start) / durationDay;
}, (date) => {
return date.getUTCDate() - 1;
});
export const utcDay = timeInterval(
(date) => date.setUTCHours(0, 0, 0, 0),
(date, step) => date.setUTCDate(date.getUTCDate() + step),
(start, end) => (end - start) / durationDay
);

export const utcDays = utcDay.range;

export const unixDay = timeInterval((date) => {
date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
date.setUTCDate(date.getUTCDate() + step);
}, (start, end) => {
return (end - start) / durationDay;
}, (date) => {
return Math.floor(date / durationDay);
});

export const unixDays = unixDay.range;
1 change: 1 addition & 0 deletions src/epoch.js
@@ -0,0 +1 @@
export const timeEpoch = new Date(1970, 0, 1);
30 changes: 12 additions & 18 deletions src/hour.js
@@ -1,26 +1,20 @@
import {timeInterval} from "./interval.js";
import {durationHour, durationMinute, durationSecond} from "./duration.js";
import {timeEpoch} from "./epoch.js";

export const timeHour = timeInterval((date) => {
date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond - date.getMinutes() * durationMinute);
}, (date, step) => {
date.setTime(+date + step * durationHour);
}, (start, end) => {
return (end - start) / durationHour;
}, (date) => {
return date.getHours();
});
export const timeHour = timeInterval(
(date) => date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond - date.getMinutes() * durationMinute),
(date, step) => date.setTime(+date + step * durationHour),
(start, end) => (end - start) / durationHour,
timeEpoch
);

export const timeHours = timeHour.range;

export const utcHour = timeInterval((date) => {
date.setUTCMinutes(0, 0, 0);
}, (date, step) => {
date.setTime(+date + step * durationHour);
}, (start, end) => {
return (end - start) / durationHour;
}, (date) => {
return date.getUTCHours();
});
export const utcHour = timeInterval(
(date) => date.setUTCMinutes(0, 0, 0),
(date, step) => date.setTime(+date + step * durationHour),
(start, end) => (end - start) / durationHour
);

export const utcHours = utcHour.range;
4 changes: 2 additions & 2 deletions src/index.js
Expand Up @@ -35,8 +35,8 @@ export {
timeDays,
utcDay,
utcDays,
unixDay,
unixDays
utcDay as unixDay, // deprecated! use utcDay
utcDays as unixDays // deprecated! use utcDays
} from "./day.js";

export {
Expand Down
8 changes: 4 additions & 4 deletions src/interval.js
@@ -1,6 +1,6 @@
const t0 = new Date, t1 = new Date;

export function timeInterval(floori, offseti, count, field) {
export function timeInterval(floori, offseti, count, epoch = 0) {

function interval(date) {
return floori(date = arguments.length === 0 ? new Date : new Date(+date)), date;
Expand Down Expand Up @@ -59,9 +59,9 @@ export function timeInterval(floori, offseti, count, field) {
step = Math.floor(step);
return !isFinite(step) || !(step > 0) ? null
: !(step > 1) ? interval
: interval.filter(field
? (d) => field(d) % step === 0
: (d) => interval.count(0, d) % step === 0);
: interval.filter(typeof epoch === "function" // deprecated field
? (d) => epoch(d) % step === 0
: (d) => interval.count(epoch, d) % step === 0);
};
}

Expand Down
30 changes: 12 additions & 18 deletions src/minute.js
@@ -1,26 +1,20 @@
import {timeInterval} from "./interval.js";
import {durationMinute, durationSecond} from "./duration.js";
import {timeEpoch} from "./epoch.js";

export const timeMinute = timeInterval((date) => {
date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond);
}, (date, step) => {
date.setTime(+date + step * durationMinute);
}, (start, end) => {
return (end - start) / durationMinute;
}, (date) => {
return date.getMinutes();
});
export const timeMinute = timeInterval(
(date) => date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond),
(date, step) => date.setTime(+date + step * durationMinute),
(start, end) => (end - start) / durationMinute,
timeEpoch
);

export const timeMinutes = timeMinute.range;

export const utcMinute = timeInterval((date) => {
date.setUTCSeconds(0, 0);
}, (date, step) => {
date.setTime(+date + step * durationMinute);
}, (start, end) => {
return (end - start) / durationMinute;
}, (date) => {
return date.getUTCMinutes();
});
export const utcMinute = timeInterval(
(date) => date.setUTCSeconds(0, 0),
(date, step) => date.setTime(+date + step * durationMinute),
(start, end) => (end - start) / durationMinute
);

export const utcMinutes = utcMinute.range;
32 changes: 12 additions & 20 deletions src/month.js
@@ -1,27 +1,19 @@
import {timeEpoch} from "./epoch.js";
import {timeInterval} from "./interval.js";

export const timeMonth = timeInterval((date) => {
date.setDate(1);
date.setHours(0, 0, 0, 0);
}, (date, step) => {
date.setMonth(date.getMonth() + step);
}, (start, end) => {
return end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12;
}, (date) => {
return date.getMonth();
});
export const timeMonth = timeInterval(
(date) => (date.setDate(1), date.setHours(0, 0, 0, 0)),
(date, step) => date.setMonth(date.getMonth() + step),
(start, end) => end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12,
timeEpoch
);

export const timeMonths = timeMonth.range;

export const utcMonth = timeInterval((date) => {
date.setUTCDate(1);
date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
date.setUTCMonth(date.getUTCMonth() + step);
}, (start, end) => {
return end.getUTCMonth() - start.getUTCMonth() + (end.getUTCFullYear() - start.getUTCFullYear()) * 12;
}, (date) => {
return date.getUTCMonth();
});
export const utcMonth = timeInterval(
(date) => (date.setUTCDate(1), date.setUTCHours(0, 0, 0, 0)),
(date, step) => date.setUTCMonth(date.getUTCMonth() + step),
(start, end) => end.getUTCMonth() - start.getUTCMonth() + (end.getUTCFullYear() - start.getUTCFullYear()) * 12
);

export const utcMonths = utcMonth.range;
14 changes: 5 additions & 9 deletions src/second.js
@@ -1,14 +1,10 @@
import {timeInterval} from "./interval.js";
import {durationSecond} from "./duration.js";

export const second = timeInterval((date) => {
date.setTime(date - date.getMilliseconds());
}, (date, step) => {
date.setTime(+date + step * durationSecond);
}, (start, end) => {
return (end - start) / durationSecond;
}, (date) => {
return date.getUTCSeconds();
});
export const second = timeInterval(
(date) => date.setTime(date - date.getMilliseconds()),
(date, step) => date.setTime(+date + step * durationSecond),
(start, end) => (end - start) / durationSecond
);

export const seconds = second.range;
4 changes: 2 additions & 2 deletions src/ticks.js
Expand Up @@ -4,7 +4,7 @@ import {millisecond} from "./millisecond.js";
import {second} from "./second.js";
import {timeMinute, utcMinute} from "./minute.js";
import {timeHour, utcHour} from "./hour.js";
import {timeDay, unixDay} from "./day.js";
import {timeDay, utcDay} from "./day.js";
import {timeSunday, utcSunday} from "./week.js";
import {timeMonth, utcMonth} from "./month.js";
import {timeYear, utcYear} from "./year.js";
Expand Down Expand Up @@ -52,7 +52,7 @@ function ticker(year, month, week, day, hour, minute) {
return [ticks, tickInterval];
}

const [utcTicks, utcTickInterval] = ticker(utcYear, utcMonth, utcSunday, unixDay, utcHour, utcMinute);
const [utcTicks, utcTickInterval] = ticker(utcYear, utcMonth, utcSunday, utcDay, utcHour, utcMinute);
const [timeTicks, timeTickInterval] = ticker(timeYear, timeMonth, timeSunday, timeDay, timeHour, timeMinute);

export {utcTicks, utcTickInterval, timeTicks, timeTickInterval};
28 changes: 12 additions & 16 deletions src/week.js
@@ -1,15 +1,14 @@
import {timeInterval} from "./interval.js";
import {durationMinute, durationWeek} from "./duration.js";
import {timeEpoch} from "./epoch.js";

function timeWeekday(i) {
return timeInterval((date) => {
date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7);
date.setHours(0, 0, 0, 0);
}, (date, step) => {
date.setDate(date.getDate() + step * 7);
}, (start, end) => {
return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationWeek;
});
return timeInterval(
(date) => (date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7), date.setHours(0, 0, 0, 0)),
(date, step) => date.setDate(date.getDate() + step * 7),
(start, end) => (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationWeek,
timeEpoch
);
}

export const timeSunday = timeWeekday(0);
Expand All @@ -29,14 +28,11 @@ export const timeFridays = timeFriday.range;
export const timeSaturdays = timeSaturday.range;

function utcWeekday(i) {
return timeInterval((date) => {
date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 7 - i) % 7);
date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
date.setUTCDate(date.getUTCDate() + step * 7);
}, (start, end) => {
return (end - start) / durationWeek;
});
return timeInterval(
(date) => (date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 7 - i) % 7), date.setUTCHours(0, 0, 0, 0)),
(date, step) => date.setUTCDate(date.getUTCDate() + step * 7),
(start, end) => (end - start) / durationWeek
);
}

export const utcSunday = utcWeekday(0);
Expand Down
54 changes: 20 additions & 34 deletions src/year.js
@@ -1,49 +1,35 @@
import {timeEpoch} from "./epoch.js";
import {timeInterval} from "./interval.js";

export const timeYear = timeInterval((date) => {
date.setMonth(0, 1);
date.setHours(0, 0, 0, 0);
}, (date, step) => {
date.setFullYear(date.getFullYear() + step);
}, (start, end) => {
return end.getFullYear() - start.getFullYear();
}, (date) => {
return date.getFullYear();
});
export const timeYear = timeInterval(
(date) => (date.setMonth(0, 1), date.setHours(0, 0, 0, 0)),
(date, step) => date.setFullYear(date.getFullYear() + step),
(start, end) => end.getFullYear() - start.getFullYear(),
timeEpoch
);

// An optimized implementation for this simple case.
timeYear.every = (k) => {
return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval((date) => {
date.setFullYear(Math.floor(date.getFullYear() / k) * k);
date.setMonth(0, 1);
date.setHours(0, 0, 0, 0);
}, (date, step) => {
date.setFullYear(date.getFullYear() + step * k);
});
return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval(
(date) => (date.setFullYear(Math.floor(date.getFullYear() / k) * k), date.setMonth(0, 1), date.setHours(0, 0, 0, 0)),
(date, step) => date.setFullYear(date.getFullYear() + step * k)
);
};

export const timeYears = timeYear.range;

export const utcYear = timeInterval((date) => {
date.setUTCMonth(0, 1);
date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
date.setUTCFullYear(date.getUTCFullYear() + step);
}, (start, end) => {
return end.getUTCFullYear() - start.getUTCFullYear();
}, (date) => {
return date.getUTCFullYear();
});
export const utcYear = timeInterval(
(date) => (date.setUTCMonth(0, 1), date.setUTCHours(0, 0, 0, 0)),
(date, step) => date.setUTCFullYear(date.getUTCFullYear() + step),
(start, end) => end.getUTCFullYear() - start.getUTCFullYear()
);

// An optimized implementation for this simple case.
utcYear.every = (k) => {
return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval((date) => {
date.setUTCFullYear(Math.floor(date.getUTCFullYear() / k) * k);
date.setUTCMonth(0, 1);
date.setUTCHours(0, 0, 0, 0);
}, (date, step) => {
date.setUTCFullYear(date.getUTCFullYear() + step * k);
});
return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval(
(date) => (date.setUTCFullYear(Math.floor(date.getUTCFullYear() / k) * k), date.setUTCMonth(0, 1), date.setUTCHours(0, 0, 0, 0)),
(date, step) => date.setUTCFullYear(date.getUTCFullYear() + step * k)
);
};

export const utcYears = utcYear.range;
8 changes: 4 additions & 4 deletions test/day-test.js
Expand Up @@ -208,8 +208,8 @@ it("timeDay.count(start, end) returns 364 or 365 for a full year", () => {
assert.strictEqual(timeDay.count(local(2011, 0, 1), local(2011, 11, 31)), 364);
});

it("timeDay.every(step) returns every stepth day, starting with the first day of the month", () => {
assert.deepStrictEqual(timeDay.every(3).range(local(2008, 11, 30, 0, 12), local(2009, 0, 5, 23, 48)), [local(2008, 11, 31), local(2009, 0, 1), local(2009, 0, 4)]);
assert.deepStrictEqual(timeDay.every(5).range(local(2008, 11, 30, 0, 12), local(2009, 0, 6, 23, 48)), [local(2008, 11, 31), local(2009, 0, 1), local(2009, 0, 6)]);
assert.deepStrictEqual(timeDay.every(7).range(local(2008, 11, 30, 0, 12), local(2009, 0, 8, 23, 48)), [local(2009, 0, 1), local(2009, 0, 8)]);
it("timeDay.every(step) returns every stepth day without resetting on the first of the month", () => {
assert.deepStrictEqual(timeDay.every(3).range(local(2008, 11, 30, 0, 12), local(2009, 0, 5, 23, 48)), [local(2008, 11, 31), local(2009, 0, 3)]);
assert.deepStrictEqual(timeDay.every(5).range(local(2008, 11, 25, 0, 12), local(2009, 0, 6, 23, 48)), [local(2008, 11, 27), local(2009, 0, 1), local(2009, 0, 6)]);
assert.deepStrictEqual(timeDay.every(7).range(local(2008, 11, 23, 0, 12), local(2009, 0, 8, 23, 48)), [local(2008, 11, 25), local(2009, 0, 1), local(2009, 0, 8)]);
});
17 changes: 13 additions & 4 deletions test/ticks-test.js
Expand Up @@ -136,6 +136,16 @@ it("timeTicks(start, stop, count) can generate 6-hour ticks", () => {
]);
});

// Note: not aligned at midnight because of daylight savings time!
it("timeTicks(start, stop, count) can generate 6-hour ticks across daylight savings time", () => {
assert.deepStrictEqual(timeTicks(local(2011, 3, 1, 16, 28, 27), local(2011, 3, 2, 14, 34, 12), 4), [
local(2011, 3, 1, 19, 0),
local(2011, 3, 2, 1, 0),
local(2011, 3, 2, 7, 0),
local(2011, 3, 2, 13, 0)
]);
});

it("timeTicks(start, stop, count) can generate 12-hour ticks", () => {
assert.deepStrictEqual(timeTicks(local(2011, 0, 1, 16, 28, 27), local(2011, 0, 3, 21, 34, 12), 4), [
local(2011, 0, 2, 0, 0),
Expand All @@ -156,10 +166,9 @@ it("timeTicks(start, stop, count) can generate 1-day ticks", () => {

it("timeTicks(start, stop, count) can generate 2-day ticks", () => {
assert.deepStrictEqual(timeTicks(local(2011, 0, 2, 16, 28, 27), local(2011, 0, 9, 21, 34, 12), 4), [
local(2011, 0, 3, 0, 0),
local(2011, 0, 5, 0, 0),
local(2011, 0, 7, 0, 0),
local(2011, 0, 9, 0, 0)
local(2011, 0, 4, 0, 0),
local(2011, 0, 6, 0, 0),
local(2011, 0, 8, 0, 0)
]);
});

Expand Down

0 comments on commit 93c5637

Please sign in to comment.