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 Aug 18, 2023
1 parent 9e8dc94 commit 642f4f7
Show file tree
Hide file tree
Showing 15 changed files with 115 additions and 164 deletions.
10 changes: 4 additions & 6 deletions README.md
Expand Up @@ -129,7 +129,7 @@ The returned filtered interval does not support [*interval*.count](#interval_cou

<a name="interval_every" href="#interval_every">#</a> <i>interval</i>.<b>every</b>(<i>step</i>) · [Source](https://github.com/d3/d3-time/blob/main/src/interval.js)

Returns a [filtered](#interval_filter) view of this interval representing every *step*th date. The meaning of *step* is dependent on this interval’s parent interval as defined by the field function. For example, [d3.timeMinute](#timeMinute).every(15) returns an interval representing every fifteen minutes, starting on the hour: :00, :15, :30, :45, <i>etc.</i> Note that for some intervals, the resulting dates may not be uniformly-spaced; [d3.timeDay](#timeDay)’s parent interval is [d3.timeMonth](#timeMonth), and thus the interval number resets at the start of each month. If *step* is not valid, returns null. If *step* is one, returns this interval.
Returns a [filtered](#interval_filter) view of this interval representing every *step*th date. ~~The meaning of *step* is dependent on this interval’s parent interval as defined by the field function.~~ For example, [d3.timeMinute](#timeMinute).every(15) returns an interval representing every fifteen minutes, starting on the hour: :00, :15, :30, :45, <i>etc.</i> ~~Note that for some intervals, the resulting dates may not be uniformly-spaced; [d3.timeDay](#timeDay)’s parent interval is [d3.timeMonth](#timeMonth), and thus the interval number resets at the start of each month.~~ TODO They are now uniformly spaced, but they may not be aligned with the parent interval; for example, [d3.timeHour](#timeHour).every(12) may not return 12AM or 12PM due to daylight savings time. If *step* is not valid, returns null. If *step* is one, returns this interval.

This method can be used in conjunction with [*interval*.range](#interval_range) to ensure that two overlapping ranges are consistent. For example, this range contains odd days:

Expand Down Expand Up @@ -169,7 +169,7 @@ The *offset* function takes a date and an integer step as arguments and advances

The optional *count* function takes a start date and an end date, already floored to the current interval, and returns the number of boundaries between the start (exclusive) and end (inclusive). If a *count* function is not specified, the returned interval does not expose [*interval*.count](#interval_count) or [*interval*.every](#interval_every) methods. Note: due to an internal optimization, the specified *count* function must not invoke *interval*.count on other time intervals.

The optional *field* function takes a date, already floored to the current interval, and returns the field value of the specified date, corresponding to the number of boundaries between this date (exclusive) and the latest previous parent boundary. For example, for the [d3.timeDay](#timeDay) interval, this returns the number of days since the start of the month. If a *field* function is not specified, it defaults to counting the number of interval boundaries since the UNIX epoch of January 1, 1970 UTC. The *field* function defines the behavior of [*interval*.every](#interval_every).
~~The optional *field* function takes a date, already floored to the current interval, and returns the field value of the specified date, corresponding to the number of boundaries between this date (exclusive) and the latest previous parent boundary. For example, for the [d3.timeDay](#timeDay) interval, this returns the number of days since the start of the month. If a *field* function is not specified, it defaults to counting the number of interval boundaries since the UNIX epoch of January 1, 1970 UTC. The *field* function defines the behavior of [*interval*.every](#interval_every).~~ TODO This is now an *epoch* argument.

### Intervals

Expand Down Expand Up @@ -197,9 +197,8 @@ Hours (e.g., 01:00 AM); 60 minutes. Note that advancing time by one hour in loca

<a name="timeDay" href="#timeDay">#</a> d3.<b>timeDay</b> · [Source](https://github.com/d3/d3-time/blob/main/src/day.js "Source")
<br><a href="#timeDay">#</a> d3.<b>utcDay</b> · [Source](https://github.com/d3/d3-time/blob/main/src/day.js)
<br><a href="#timeDay">#</a> d3.<b>unixDay</b> · [Source](https://github.com/d3/d3-time/blob/main/src/day.js)

Days (e.g., February 7, 2012 at 12:00 AM); typically 24 hours. Days in local time may range from 23 to 25 hours due to daylight saving. d3.unixDay is like [d3.utcDay](#timeDay), except it counts days since the UNIX epoch (January 1, 1970) such that *interval*.every returns uniformly-spaced dates rather than varying based on day-of-month.
Days (e.g., February 7, 2012 at 12:00 AM); typically 24 hours. Days in local time may range from 23 to 25 hours due to daylight saving.

<a name="timeWeek" href="#timeWeek">#</a> d3.<b>timeWeek</b> · [Source](https://github.com/d3/d3-time/blob/main/src/week.js "Source")
<br><a href="#timeWeek">#</a> d3.<b>utcWeek</b> · [Source](https://github.com/d3/d3-time/blob/main/src/utcWeek.js "Source")
Expand Down Expand Up @@ -277,9 +276,8 @@ Aliases for [d3.timeHour](#timeHour).[range](#interval_range) and [d3.utcHour](#

<a name="timeDays" href="#timeDays">#</a> d3.<b>timeDays</b>(<i>start</i>, <i>stop</i>[, <i>step</i>]) · [Source](https://github.com/d3/d3-time/blob/main/src/day.js)
<br><a href="#timeDays">#</a> d3.<b>utcDays</b>(<i>start</i>, <i>stop</i>[, <i>step</i>]) · [Source](https://github.com/d3/d3-time/blob/main/src/day.js)
<br><a href="#timeDays">#</a> d3.<b>unixDays</b>(<i>start</i>, <i>stop</i>[, <i>step</i>]) · [Source](https://github.com/d3/d3-time/blob/main/src/day.js)

Aliases for [d3.timeDay](#timeDay).[range](#interval_range), [d3.utcDay](#timeDay).[range](#interval_range), and [d3.unixDay](#timeDay).[range](#interval_range).
Aliases for [d3.timeDay](#timeDay).[range](#interval_range) and [d3.utcDay](#timeDay).[range](#interval_range).

<a name="timeWeeks" href="#timeWeeks">#</a> d3.<b>timeWeeks</b>(<i>start</i>, <i>stop</i>[, <i>step</i>])
<br><a href="#timeWeeks">#</a> d3.<b>utcWeeks</b>(<i>start</i>, <i>stop</i>[, <i>step</i>])
Expand Down
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)]);
});

0 comments on commit 642f4f7

Please sign in to comment.