-
Notifications
You must be signed in to change notification settings - Fork 155
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
Calendars can't accept fields in from()
or with()
that are not emitted by getFields()
#1235
Comments
A related problem I found today: One way to handle this (probably the easiest and perhaps the best) would be to move the merging of new vs. existing date fields into the calendar implementation, which I assume would mean extending the CalendarProtocol API, e.g. a new EDIT: another possible solution might be to add a third "role" in the
This seems to be cleaner and easier than my first proposal above because doesn't grow the CalendarProtocol API surface and concentrates all non-ISO-calendar-related changes to the |
I feel OK with the track opened per (2). I think example usage will be easy to explain. |
Most of the decisions about processing option bags date back to before we supported dynamically changing the supported fields, so I don't see a problem with reconsidering. The impact is pretty limited anyway. Right now, the calendar-independent code does a bunch of preprocessing and passes fairly normalized values to the (built-in or author-implemented) Calendar methods. This is mostly for convenience when implementing a custom calendar (as the implementation doesn't need to be as robust to weird input), and for predictability when using a custom calendar (since weird inputs are handled uniformly); both of those reasons apply only when calling through the main objects - if you call the Calendar methods directly, you're on your own. If it turns out this is blocking useful functionality, I don't mind dropping the preprocessing entirely. That would solve |
in my upcoming PR for basic implementations of ICU calendars, I'm currently using this proposal from above:
So far it's been working OK. Here's the current implementation: fields(fields, role) {
// Some non-ISO calendars accept multiple ways to specify a year and/or a
// month. Here's where we add this support for all calendars (including
// ISO), which makes it easier to write cross-calendar code. Note that this
// only affects only how input is interpreted. `getFields()` output is not
// affected.
if (role === 'input') {
if (fields.includes('year')) {
fields.push('era');
fields.push('eraYear');
}
if (fields.includes('month')) {
fields.push('monthCode');
}
} else if (role === 'merge') {
// If the user includes one of `fields` as input to `with()`, then the
// returned fields array will be not be pulled from this before `with()`
// does the merge. For example, if the user calls `date.with({monthCode})`
// then `month` will be excluded from the merge so the input to the
// calendar's `dateFromFields()` method will be `{day, monthCode, year}`
// not `{day, month, monthCode, year}` which would be problematic because
// the month values could conflict and the calendar wouldn't know which
// one to use.
const removeFields = [];
if (fields.includes('year')) {
removeFields.push('era');
removeFields.push('eraYear');
removeFields.push('year');
}
if (fields.includes('eraYear')) {
removeFields.push('eraYear');
removeFields.push('year');
}
if (fields.includes('month') || fields.includes('monthCode')) {
removeFields.push('month');
removeFields.push('monthCode');
}
return removeFields;
}
return fields;
}, To get the new CalendarFields: (calendar, fieldNames, role = 'input') => {
let fields = calendar.fields;
if (fields === undefined) fields = GetIntrinsic('%Temporal.Calendar.prototype.fields%');
const array = ES.Call(fields, calendar, [fieldNames, { role }]);
return ES.CreateListFromArrayLike(array, ['String']);
},
MergeFields: (calendar, fields, props) => {
const doNotMergeNames = ES.CalendarFields(calendar, ES.GetOwnPropertyKeys(props, 'String'), 'merge');
let merged = ObjectAssign({}, fields);
for (const prop of doNotMergeNames) {
delete merged[prop];
}
ObjectAssign(merged, props);
return merged;
}, And here's how it's used in const calendar = GetSlot(this, CALENDAR);
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
const props = ES.ToPartialRecord(temporalDateLike, fieldNames);
if (!props) {
throw new TypeError('invalid date-like');
}
let fields = ES.ToTemporalDateFields(this, fieldNames);
fields = ES.MergeFields(calendar, fields, props); And also here: getFields() {
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
const calendar = GetSlot(this, CALENDAR);
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year'], 'output');
const fields = ES.ToTemporalDateFields(this, fieldNames);
fields.calendar = calendar;
return fields;
} Finally, I modified the calendar-calling // field access in the following operations is intentionally alphabetical
ToTemporalDateFields: (bag, fieldNames) => {
const entries = [
['day', undefined],
['month', undefined],
['year', undefined]
];
// Add extra fields from the calendar at the end
fieldNames.forEach((fieldName) => {
if (!entries.some(([name]) => name === fieldName)) {
entries.push([fieldName, undefined]);
}
});
return ES.ToRecord(bag, entries, false);
}, One change I'm planning to make: instead of after-the-fact Assuming that change is made, @Ms2ger what do you think of this approach above? |
I do think this is a finding from our reviews that we need to address, but I'm not convinced that the
|
Unless I misunderstood the logic flow in the polyfill, I don't think we can make
The problem is that in step (4), how does the calendar know which fields were provided by the user and which ones come from Temporal.PlainDate.from({ year: 5760, month: 1, day: 1, calendar: 'hebrew'}).with({monthCode: '5' }); The bag passed to I guess one solution is to say that calendars can't have conflicting fields. All fields must be mutually-exclusive and non-overlapping. But based on what I learned from #1245, many calendars have overlapping ways to refer to years and months. I agree that the other role ( |
Conclusions from 2021-01-19 meeting:
I will close this issue and open two new ones for those actions (remove getFields(), and solve with().) |
The
Calendar.prototype.fields
method is currently used for two purposes:a) to determine the list of fields emitted by
getFields()
b) to constrain the list of fields accepted by
with()
and the property-bag input offrom()
.This means that calendars cannot accept any fields in
from()
orwith()
input that are not also present ingetFields()
output. This is problematic because calendars may want to offer flexible ways to specify dates. For example:eraYear: 100, era: 'bc'
, or via a single signed epoch-relative year value e.g.year: -99
6
for Adar I, the Hebrew leap month), or as a stringmonthCode
like '5L'.The simplest way to solve this problem would be to simply not use
Calendar.prototype.fields
in the implementation offrom()
orwith
. Instead, we'd just pass the bag input straight through to the calendar. This works OK forfrom
in PlainDate, MonthDay, and YearMonth where the calendar is the only code that needs to access those date fields (per #1229). I think (although not 100% sure) that the same approach will work forwith
too on these types.But if
ZonedDateTime.from
andPlainDateTime.from
used that pass-through approach, then there'd be no way for those methods to guarantee that field access happens in alphabetical order per #736 (comment), because the calendar will access the date fields only, and the time fields will be accessed separately (and out of order from the date fields) inside ecmascript.mjs.If we do want to enable calendars to accept arbitrary fields as input, here's a few possible options:
from()
call should always access fields of input objects in alphabetical order.Calendar.prototype.fields
to indicate whether the field list will be used for input (from
orwith
) or for output (getFields
).getFields
, even if they overlap with other output fields, e.g.month
andmonthCode
in the same output bag. I'm skeptical about this option because it'd introduce the possibility of field conflicts if thegetFields()
output is modified and then sent back into Temporal, e.g. if a developer changesmonth
but forgets to also changemonthCode
. Also, this would prevent calendars from offering an opinionated canonical set of fields while still being flexible about alternate/aliased forms, e.g. Chinese named years.(2) seems like the easiest and most flexible solution. Most folks are probably ignoring GitHub issues for the holidays, so unless anyone has a strong opinion about this I'll plan to adopt (2) to avoid blocking my almost-complete non-ISO calendar PR. We can always revise later as needed.
The text was updated successfully, but these errors were encountered: