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

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

Open
mbostock opened this issue Sep 20, 2021 · 6 comments
Open

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

mbostock opened this issue Sep 20, 2021 · 6 comments
Labels
documentation Additions to documentation
Milestone

Comments

@mbostock
Copy link

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!

@ljharb
Copy link
Member

ljharb commented Sep 20, 2021

Set and Map in JS use object identity - it seems like:

InternMap under the hood, which coerces keys to a primitive value via valueOf.

is the problem here? Not all objects are serializable to a "hash".

@jethrolarson
Copy link

I'm no expert here but I would think that Temporal objects will work really well as keys since they're immutable.

const myMap = new Map()
const d = Temporal.ZonedDateTime.from({timeZone: "UTC", year: 2021, month: 1, day: 2})
myMap.set(d, 'foo')
myMap.get(d)

@ljharb
Copy link
Member

ljharb commented Sep 28, 2021

Mutability has no effect on their use as keys; the way they'd be most useful as keys is if two calls to Temporal.ZonedDateTime.from({timeZone: "UTC", year: 2021, month: 1, day: 2}) produced the same identity - but that's not how the proposal works.

@ptomato
Copy link
Collaborator

ptomato commented Oct 1, 2021

It seems like the outcome of this issue should be a cookbook recipe showing what the recommended way to do this would be. (And if it turns out to be incredibly cumbersome, we should take that seriously as ecosystem feedback; this seems like a reasonably common use case.)

I agree that calling valueOf indiscriminately on objects is probably not something that InternMap/InternSet can rely on to get a hashable value, so that is probably something that should be brought to that library's attention 😄

@ptomato ptomato added the documentation Additions to documentation label Oct 1, 2021
@mbostock
Copy link
Author

mbostock commented Oct 1, 2021

I agree that calling valueOf indiscriminately on objects is probably not something that InternMap/InternSet can rely on to get a hashable value, so that is probably something that should be brought to that library's attention

Hi, that’s me. 👋 What I’m looking for is a generic recommendation on how to use timestamps (referring generically here to Date and Temporal instances… or perhaps just Temporal instants since those do have an obvious primitive representation of epoch nanoseconds) as keys in Maps or values in Sets. I could of course do instanceof checks and only support specific classes, but especially given that Temporal isn’t available natively yet and needs to be polyfilled, that would be rather limiting. I’d love some hints on how best to support this use case universally in D3 and similar libraries that work with temporal data.

@ljharb
Copy link
Member

ljharb commented Oct 1, 2021

@mbostock as far as I'm aware, there is no generic way to do what you want, and explicitly supporting each thing individually is the only robust approach.

@ptomato ptomato added this to the Post Stage 4 milestone Dec 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Additions to documentation
Projects
None yet
Development

No branches or pull requests

4 participants