Skip to content

How to use Temporal instances as Map keys or Set values? #1840

@mbostock

Description

@mbostock

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationAdditions to documentation

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions