Skip to content
Permalink
Browse files
Implement Temporal.PlainDate#{since, until}
https://bugs.webkit.org/show_bug.cgi?id=245550

Reviewed by Yusuke Suzuki.

This patch implements the `since` / `until` methods for PlainDate (and accordingly the `dateUntil` method for Calendar).

Since we haven't implemented `relativeTo` support for Duration yet, we must postpone implementation of:
  1. the path where `smallestUnit` is set to `year`, `month`, or `week`
  2. the entirety of `since` / `until` for PlainDateTime

Otherwise, this completes the "ISO8601-only" implementation for PlainDate and PlainDateTime.

* JSTests/stress/temporal-calendar.js:
* JSTests/stress/temporal-plaindate.js:
* JSTests/test262/config.yaml:
* JSTests/test262/expectations.yaml:
* Source/JavaScriptCore/runtime/TemporalCalendar.cpp:
(JSC::TemporalCalendar::isoDateDifference): Added.
(JSC::TemporalCalendar::isoDateCompare): Moved from TemporalPlainDate.
* Source/JavaScriptCore/runtime/TemporalCalendar.h:
* Source/JavaScriptCore/runtime/TemporalCalendarPrototype.cpp:
* Source/JavaScriptCore/runtime/TemporalObject.cpp:
(JSC::extractDifferenceOptions): Moved from TemporalPlainTime.
(JSC::negateTemporalRoundingMode): Added.
* Source/JavaScriptCore/runtime/TemporalObject.h:
* Source/JavaScriptCore/runtime/TemporalPlainDate.cpp:
(JSC::TemporalPlainDate::until): Added.
(JSC::TemporalPlainDate::since): Added.
(JSC::TemporalPlainDate::compare): Moved to TemporalCalendar.
* Source/JavaScriptCore/runtime/TemporalPlainDate.h:
* Source/JavaScriptCore/runtime/TemporalPlainDateConstructor.cpp:
* Source/JavaScriptCore/runtime/TemporalPlainDatePrototype.cpp:
* Source/JavaScriptCore/runtime/TemporalPlainDateTime.cpp:
(JSC::TemporalPlainDateTime::compare):
* Source/JavaScriptCore/runtime/TemporalPlainTime.cpp:
(JSC::TemporalPlainTime::until const):
(JSC::TemporalPlainTime::since const):
(JSC::extractDifferenceOptions): Moved to TemporalObject.

Canonical link: https://commits.webkit.org/254780@main
  • Loading branch information
rkirsling committed Sep 23, 2022
1 parent 7b1a4e7 commit 0c4378330b67f1dc802edd25a80a78f446b3be09
Show file tree
Hide file tree
Showing 15 changed files with 483 additions and 142 deletions.
@@ -137,3 +137,14 @@ shouldBe(isoCalendar.dateAdd('2020-02-28', new Temporal.Duration(1, 1, 0, 1)).to
shouldBe(isoCalendar.dateAdd('2020-03-30', { months: -1 }).toString(), '2020-02-29');
shouldThrow(() => { isoCalendar.dateAdd('2020-03-30', { months: -1 }, { overflow: 'reject' }); }, RangeError);
shouldThrow(() => { isoCalendar.dateAdd('2020-02-28', { years: -300000 }); }, RangeError);

shouldBe(Temporal.Calendar.prototype.dateUntil.length, 2);
shouldBe(isoCalendar.dateUntil('2020-02-28', '2019-02-28').toString(), '-P365D');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2019-02-28', { largestUnit: 'year' }).toString(), '-P1Y');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2021-02-28').toString(), 'P366D');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2021-02-28', { largestUnit: 'year' }).toString(), 'P1Y');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2020-04-28', { largestUnit: 'month' }).toString(), 'P2M');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2019-12-28', { largestUnit: 'month' }).toString(), '-P2M');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2020-03-15', { largestUnit: 'week' }).toString(), 'P2W2D');
shouldBe(isoCalendar.dateUntil('2020-02-28', '2020-02-12', { largestUnit: 'week' }).toString(), '-P2W2D');
shouldThrow(() => { isoCalendar.dateUntil('2020-02-28', '2019-02-28', { largestUnit: 'hour' }); }, RangeError);
@@ -385,3 +385,34 @@ shouldBe(Temporal.PlainDate.prototype.with.length, 1);
shouldBe(date.with({ month: 2 }).toString(), '2020-02-29');
shouldThrow(() => { date.with({ month: 2 }, { overflow: 'reject' }); }, RangeError);
}

shouldBe(Temporal.PlainDate.prototype.since.length, 1);
shouldBe(Temporal.PlainDate.prototype.until.length, 1);
{
const date = Temporal.PlainDate.from('2020-02-28');

shouldBe(date.since('2019-02-28').toString(), 'P365D');
shouldBe(date.until('2019-02-28').toString(), '-P365D');
shouldBe(date.since('2019-02-28', { largestUnit: 'year' }).toString(), 'P1Y');
shouldBe(date.until('2019-02-28', { largestUnit: 'year' }).toString(), '-P1Y');
shouldBe(date.since('2021-02-28').toString(), '-P366D');
shouldBe(date.until('2021-02-28').toString(), 'P366D');
shouldBe(date.since('2021-02-28', { largestUnit: 'year' }).toString(), '-P1Y');
shouldBe(date.until('2021-02-28', { largestUnit: 'year' }).toString(), 'P1Y');

shouldBe(date.since('2020-04-28', { largestUnit: 'month' }).toString(), '-P2M');
shouldBe(date.until('2020-04-28', { largestUnit: 'month' }).toString(), 'P2M');
shouldBe(date.since('2019-12-28', { largestUnit: 'month' }).toString(), 'P2M');
shouldBe(date.until('2019-12-28', { largestUnit: 'month' }).toString(), '-P2M');

shouldBe(date.since('2020-03-15', { largestUnit: 'week' }).toString(), '-P2W2D');
shouldBe(date.until('2020-03-15', { largestUnit: 'week' }).toString(), 'P2W2D');
shouldBe(date.since('2020-03-15', { roundingMode: 'halfExpand', roundingIncrement: 3 }).toString(), '-P15D');
shouldBe(date.until('2020-03-15', { roundingMode: 'halfExpand', roundingIncrement: 3 }).toString(), 'P15D');
shouldBe(date.since('2020-02-12', { largestUnit: 'week' }).toString(), 'P2W2D');
shouldBe(date.until('2020-02-12', { largestUnit: 'week' }).toString(), '-P2W2D');

shouldThrow(() => { date.until('2019-02-28', { smallestUnit: 'hour' }); }, RangeError);
shouldThrow(() => { date.until('2019-02-28', { largestUnit: 'hour' }); }, RangeError);
shouldThrow(() => { date.until('2019-02-28', { largestUnit: 'day', smallestUnit: 'month' }); }, RangeError);
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -339,6 +339,99 @@ ISO8601::PlainDate TemporalCalendar::isoDateAdd(JSGlobalObject* globalObject, co
return result;
}

// https://tc39.es/proposal-temporal/#sec-temporal-differenceisodate
ISO8601::Duration TemporalCalendar::isoDateDifference(JSGlobalObject* globalObject, const ISO8601::PlainDate& date1, const ISO8601::PlainDate& date2, TemporalUnit largestUnit)
{
ASSERT(largestUnit <= TemporalUnit::Day);

VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

if (largestUnit <= TemporalUnit::Month) {
auto sign = isoDateCompare(date2, date1);
if (!sign)
return { };

double years = date2.year() - date1.year();
ISO8601::PlainDate mid = isoDateAdd(globalObject, date1, { years, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, TemporalOverflow::Constrain);
RETURN_IF_EXCEPTION(scope, { });
auto midSign = isoDateCompare(date2, mid);
if (!midSign) {
if (largestUnit == TemporalUnit::Month)
return { 0, 12 * years, 0, 0, 0, 0, 0, 0, 0, 0 };

return { years, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
}

double months = date2.month() - date1.month();
if (midSign != sign) {
years -= sign;
months += 12 * sign;
}
mid = isoDateAdd(globalObject, date1, { years, months, 0, 0, 0, 0, 0, 0, 0, 0 }, TemporalOverflow::Constrain);
RETURN_IF_EXCEPTION(scope, { });
midSign = isoDateCompare(date2, mid);
if (!midSign) {
if (largestUnit == TemporalUnit::Month)
return { 0, months + 12 * years, 0, 0, 0, 0, 0, 0, 0, 0 };

return { years, months, 0, 0, 0, 0, 0, 0, 0, 0 };
}

if (midSign != sign) {
months -= sign;
if (months == -sign) {
years -= sign;
months = 11 * sign;
}
mid = isoDateAdd(globalObject, date1, { years, months, 0, 0, 0, 0, 0, 0, 0, 0 }, TemporalOverflow::Constrain);
RETURN_IF_EXCEPTION(scope, { });
}

double days;
if (mid.month() == date2.month())
days = date2.day() - mid.day();
else if (sign < 0)
days = -mid.day() - (ISO8601::daysInMonth(date2.year(), date2.month()) - date2.day());
else
days = date2.day() + (ISO8601::daysInMonth(mid.year(), mid.month()) - mid.day());

if (largestUnit == TemporalUnit::Month)
return { 0, months + 12 * years, 0, days, 0, 0, 0, 0, 0, 0 };

return { years, months, 0, days, 0, 0, 0, 0, 0, 0 };
}

double days = dateToDaysFrom1970(static_cast<int>(date2.year()), static_cast<int>(date2.month() - 1), static_cast<int>(date2.day()))
- dateToDaysFrom1970(static_cast<int>(date1.year()), static_cast<int>(date1.month() - 1), static_cast<int>(date1.day()));

double weeks = 0;
if (largestUnit == TemporalUnit::Week) {
weeks = std::trunc(days / 7);
days = std::fmod(days, 7) + 0.0;
}

return { 0, 0, weeks, days, 0, 0, 0, 0, 0, 0 };
}

// https://tc39.es/proposal-temporal/#sec-temporal-compareisodate
int32_t TemporalCalendar::isoDateCompare(const ISO8601::PlainDate& d1, const ISO8601::PlainDate& d2)
{
if (d1.year() > d2.year())
return 1;
if (d1.year() < d2.year())
return -1;
if (d1.month() > d2.month())
return 1;
if (d1.month() < d2.month())
return -1;
if (d1.day() > d2.day())
return 1;
if (d1.day() < d2.day())
return -1;
return 0;
}

bool TemporalCalendar::equals(JSGlobalObject* globalObject, TemporalCalendar* other)
{
VM& vm = globalObject->vm();
@@ -52,6 +52,8 @@ class TemporalCalendar final : public JSNonFinalObject {
static ISO8601::PlainDate isoDateFromFields(JSGlobalObject*, JSObject*, TemporalOverflow);
static ISO8601::PlainDate isoDateFromFields(JSGlobalObject*, double year, double month, double day, TemporalOverflow);
static ISO8601::PlainDate isoDateAdd(JSGlobalObject*, const ISO8601::PlainDate&, const ISO8601::Duration&, TemporalOverflow);
static ISO8601::Duration isoDateDifference(JSGlobalObject*, const ISO8601::PlainDate&, const ISO8601::PlainDate&, TemporalUnit);
static int32_t isoDateCompare(const ISO8601::PlainDate&, const ISO8601::PlainDate&);

CalendarID identifier() const { return m_identifier; }
bool isISO8601() const { return m_identifier == iso8601CalendarID(); }
@@ -39,6 +39,7 @@ namespace JSC {

static JSC_DECLARE_HOST_FUNCTION(temporalCalendarPrototypeFuncDateFromFields);
static JSC_DECLARE_HOST_FUNCTION(temporalCalendarPrototypeFuncDateAdd);
static JSC_DECLARE_HOST_FUNCTION(temporalCalendarPrototypeFuncDateUntil);
static JSC_DECLARE_HOST_FUNCTION(temporalCalendarPrototypeFuncFields);
static JSC_DECLARE_HOST_FUNCTION(temporalCalendarPrototypeFuncMergeFields);
static JSC_DECLARE_HOST_FUNCTION(temporalCalendarPrototypeFuncToString);
@@ -57,6 +58,7 @@ const ClassInfo TemporalCalendarPrototype::s_info = { "Temporal.Calendar"_s, &Ba
@begin temporalCalendarPrototypeTable
dateFromFields temporalCalendarPrototypeFuncDateFromFields DontEnum|Function 1
dateAdd temporalCalendarPrototypeFuncDateAdd DontEnum|Function 2
dateUntil temporalCalendarPrototypeFuncDateUntil DontEnum|Function 2
fields temporalCalendarPrototypeFuncFields DontEnum|Function 1
mergeFields temporalCalendarPrototypeFuncMergeFields DontEnum|Function 2
toString temporalCalendarPrototypeFuncToString DontEnum|Function 0
@@ -157,6 +159,39 @@ JSC_DEFINE_HOST_FUNCTION(temporalCalendarPrototypeFuncDateAdd, (JSGlobalObject*
RELEASE_AND_RETURN(scope, JSValue::encode(TemporalPlainDate::create(vm, globalObject->plainDateStructure(), WTFMove(plainDate))));
}

// https://tc39.es/proposal-temporal/#sup-temporal.calendar.prototype.dateuntil
JSC_DEFINE_HOST_FUNCTION(temporalCalendarPrototypeFuncDateUntil, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

auto* calendar = jsDynamicCast<TemporalCalendar*>(callFrame->thisValue());
if (!calendar)
return throwVMTypeError(globalObject, scope, "Temporal.Calendar.prototype.dateUntil called on value that's not a Calendar"_s);

// FIXME: Implement after fleshing out the rest of Temporal.Calendar.
if (!calendar->isISO8601())
return throwVMRangeError(globalObject, scope, "unimplemented: non-ISO8601 calendar"_s);

auto* date1 = TemporalPlainDate::from(globalObject, callFrame->argument(0), std::nullopt);
RETURN_IF_EXCEPTION(scope, { });

auto* date2 = TemporalPlainDate::from(globalObject, callFrame->argument(1), std::nullopt);
RETURN_IF_EXCEPTION(scope, { });

JSObject* options = intlGetOptionsObject(globalObject, callFrame->argument(2));
RETURN_IF_EXCEPTION(scope, { });

auto largest = temporalLargestUnit(globalObject, options, { TemporalUnit::Hour, TemporalUnit::Minute, TemporalUnit::Second, TemporalUnit::Millisecond, TemporalUnit::Microsecond, TemporalUnit::Nanosecond }, TemporalUnit::Day);
RETURN_IF_EXCEPTION(scope, { });
TemporalUnit largestUnit = largest.value_or(TemporalUnit::Day);

auto result = TemporalCalendar::isoDateDifference(globalObject, date1->plainDate(), date2->plainDate(), largestUnit);
RETURN_IF_EXCEPTION(scope, { });

RELEASE_AND_RETURN(scope, JSValue::encode(TemporalDuration::tryCreateIfValid(globalObject, WTFMove(result), globalObject->durationStructure())));
}

// https://tc39.es/proposal-temporal/#sup-temporal.calendar.prototype.fields
JSC_DEFINE_HOST_FUNCTION(temporalCalendarPrototypeFuncFields, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
@@ -300,6 +300,44 @@ std::optional<TemporalUnit> temporalSmallestUnit(JSGlobalObject* globalObject, J
return unitType;
}

static constexpr std::initializer_list<TemporalUnit> disallowedUnits[] = {
{ },
{ TemporalUnit::Hour, TemporalUnit::Minute, TemporalUnit::Second, TemporalUnit::Millisecond, TemporalUnit::Microsecond, TemporalUnit::Nanosecond },
{ TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day }
};

// https://tc39.es/proposal-temporal/#sec-temporal-getdifferencesettings
std::tuple<TemporalUnit, TemporalUnit, RoundingMode, double> extractDifferenceOptions(JSGlobalObject* globalObject, JSValue optionsValue, UnitGroup unitGroup, TemporalUnit defaultSmallestUnit, TemporalUnit defaultLargestUnit)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

JSObject* options = intlGetOptionsObject(globalObject, optionsValue);
RETURN_IF_EXCEPTION(scope, { });

auto smallest = temporalSmallestUnit(globalObject, options, disallowedUnits[static_cast<uint8_t>(unitGroup)]);
RETURN_IF_EXCEPTION(scope, { });
TemporalUnit smallestUnit = smallest.value_or(defaultSmallestUnit);
defaultLargestUnit = std::min(defaultLargestUnit, smallestUnit);

auto largest = temporalLargestUnit(globalObject, options, disallowedUnits[static_cast<uint8_t>(unitGroup)], defaultLargestUnit);
RETURN_IF_EXCEPTION(scope, { });
TemporalUnit largestUnit = largest.value_or(defaultLargestUnit);

if (smallestUnit < largestUnit) {
throwRangeError(globalObject, scope, "smallestUnit must be smaller than largestUnit"_s);
return { };
}

auto roundingMode = temporalRoundingMode(globalObject, options, RoundingMode::Trunc);
RETURN_IF_EXCEPTION(scope, { });

auto increment = temporalRoundingIncrement(globalObject, options, maximumRoundingIncrement(smallestUnit), false);
RETURN_IF_EXCEPTION(scope, { });

return { smallestUnit, largestUnit, roundingMode, increment };
}

// GetStringOrNumberOption(normalizedOptions, "fractionalSecondDigits", « "auto" », 0, 9, "auto")
// https://tc39.es/proposal-temporal/#sec-getstringornumberoption
std::optional<unsigned> temporalFractionalSecondDigits(JSGlobalObject* globalObject, JSObject* options)
@@ -399,6 +437,19 @@ RoundingMode temporalRoundingMode(JSGlobalObject* globalObject, JSObject* option
"roundingMode must be either \"ceil\", \"floor\", \"trunc\", or \"halfExpand\""_s, fallback);
}

// https://tc39.es/proposal-temporal/#sec-temporal-negatetemporalroundingmode
RoundingMode negateTemporalRoundingMode(RoundingMode roundingMode)
{
switch (roundingMode) {
case RoundingMode::Ceil:
return RoundingMode::Floor;
case RoundingMode::Floor:
return RoundingMode::Ceil;
default:
return roundingMode;
}
}

void formatSecondsStringFraction(StringBuilder& builder, unsigned fraction, std::tuple<Precision, unsigned> precision)
{
auto [precisionType, precisionValue] = precision;
@@ -109,16 +109,24 @@ struct PrecisionData {
unsigned increment;
};

enum class UnitGroup : uint8_t {
DateTime,
Date,
Time,
};

double nonNegativeModulo(double x, double y);
WTF::String ellipsizeAt(unsigned maxLength, const WTF::String&);
PropertyName temporalUnitPluralPropertyName(VM&, TemporalUnit);
PropertyName temporalUnitSingularPropertyName(VM&, TemporalUnit);
std::optional<TemporalUnit> temporalUnitType(StringView);
std::optional<TemporalUnit> temporalLargestUnit(JSGlobalObject*, JSObject* options, std::initializer_list<TemporalUnit> disallowedUnits, TemporalUnit autoValue);
std::optional<TemporalUnit> temporalSmallestUnit(JSGlobalObject*, JSObject* options, std::initializer_list<TemporalUnit> disallowedUnits);
std::tuple<TemporalUnit, TemporalUnit, RoundingMode, double> extractDifferenceOptions(JSGlobalObject*, JSValue, UnitGroup, TemporalUnit defaultSmallestUnit, TemporalUnit defaultLargestUnit);
std::optional<unsigned> temporalFractionalSecondDigits(JSGlobalObject*, JSObject* options);
PrecisionData secondsStringPrecision(JSGlobalObject*, JSObject* options);
RoundingMode temporalRoundingMode(JSGlobalObject*, JSObject*, RoundingMode);
RoundingMode negateTemporalRoundingMode(RoundingMode);
void formatSecondsStringFraction(StringBuilder&, unsigned fraction, std::tuple<Precision, unsigned>);
void formatSecondsStringPart(StringBuilder&, unsigned second, unsigned fraction, PrecisionData);
std::optional<double> maximumRoundingIncrement(TemporalUnit);

0 comments on commit 0c43783

Please sign in to comment.