-
Notifications
You must be signed in to change notification settings - Fork 174
Description
Hello Temporal proposal authors! I have a question about how to use the Temporal API with Map and Set.
As context, I am the author of D3.js, an open-source library for data visualization. Visualization often involves time-series data, so dates and times are an important part of D3: see e.g. d3-time and d3-time-format. Common tasks with time-series data including grouping and querying data by date, say to join two tables using a date key.
For example, say you have a tabular dataset of customer purchases via a JSON API, something like:
[
{"customer_id": "4102398", "time": "2021-01-02T20:30:56.000Z", "amount_cents": 13023},
{"customer_id": "41231245", "time": "2021-01-02T20:31:12.000Z", "amount_cents": 4123},
{"customer_id": "12308314", "time": "2021-01-02T20:31:34.000Z", "amount_cents": 3124},
{"customer_id": "1398", "time": "2021-01-03T02:01:36.000Z", "amount_cents": 1234}
]You might convert this into a more convenient JavaScript representation using Date like so:
const purchases = json.map(({time, ...d}) => ({time: new Date(time), ...d}));Now say you want to group purchases by date. Here I’ll use UTC midnight to define the start and end of each day interval. Using d3.group and d3.utcDay, you can say:
const purchasesByDate = d3.group(purchases, d => d3.utcDay(d.time));Now to get the purchases on January 2, 2021, UTC:
purchasesByDate.get(new Date("2021-01-02"))Similarly, using d3.rollup, you can say:
const totalPurchasesByDate = d3.rollup(purchases, D => d3.sum(D, d => d.amount_cents), d => d3.utcDay(d.time));This works because d3.group and d3.rollup use InternMap under the hood, which coerces keys to a primitive value via valueOf. (I could have passed 1609545600000 instead of the Date instance to get above.)
However, Temporal instances eschew valueOf (previously #74 #517 #1462), and hence trying to use a Temporal.Instant as a key in an InternMap (or a value in an InternSet) will throw an error. I could put some special magic in InternMap or InternSet to handle objects whose valueOf methods throw an Error, and, say, fallback to toJSON as the key. Or even special-case Temporal instances (similar to how JavaScript special-cases [[defaultValue]] for Date). But is this what you recommend?
Converting to a primitive value and back again so that dates can be used as keys with Map is somewhat cumbersome. For example, with the current Temporal.Instant, I’d say:
const purchases = json.map(({time, ...d}) => ({
time: Temporal.Instant.from(time),
...d
}));const purchasesByDate = d3.group(
purchases,
d => d.time.toZonedDateTimeISO("UTC")
.round({smallestUnit: "day", roundingMode: "floor"})
.toJSON()
)const totalPurchasesByDate = d3.rollup(
purchases,
D => d3.sum(D, d => d.amount_cents),
d => d.time.toZonedDateTimeISO("UTC")
.round({smallestUnit: "day", roundingMode: "floor"})
.toJSON()
);Then to lookup a value, I’d either need to hard-code the string format:
purchasesByDate.get("2021-01-02T00:00:00+00:00[UTC]")Or use toJSON:
purchasesByDate.get(Temporal.ZonedDateTime.from({timeZone: "UTC", year: 2021, month: 1, day: 2}).toJSON())And similarly when iterating over the Map, if I want a nice representation of the key as a Temporal.ZonedDateTime, I’d need to say:
for (const [timeKey, purchases] of purchasesByDate) {
const time = Temporal.ZonedDateTime.from(timeKey);
console.log(time, purchases);
}Arguably this is mostly a limitation of the Map and Set collections in JavaScript, which don’t allow keys and values to define equality (or hashCode, as Java does). But it’s still a common use case, so I’d love any perspective you may have on how to do this elegantly with the proposed Temporal API. Thanks for your time!