diff --git a/docs/absolute.md b/docs/absolute.md index 2efdbad5a8..3abcedaad2 100644 --- a/docs/absolute.md +++ b/docs/absolute.md @@ -254,6 +254,8 @@ If you need to do this, convert the `Temporal.Absolute` to a `Temporal.DateTime` If the result is earlier or later than the range that `Temporal.Absolute` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), a `RangeError` will be thrown. +Adding a negative duration is equivalent to subtracting the absolute value of that duration. + Example usage: ```js // Temporal.Absolute representing five hours from now @@ -279,6 +281,8 @@ If you need to do this, convert the `Temporal.Absolute` to a `Temporal.DateTime` If the result is earlier or later than the range that `Temporal.Absolute` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), a `RangeError` will be thrown. +Subtracting a negative duration is equivalent to adding the absolute value of that duration. + Example usage: ```js // Temporal.Absolute representing this time yesterday diff --git a/docs/balancing.md b/docs/balancing.md index 7e881a8601..e0c61fc4c4 100644 --- a/docs/balancing.md +++ b/docs/balancing.md @@ -27,34 +27,34 @@ Therefore, any `Duration` object with nonzero years or months can refer to a dif No balancing is ever performed between years, months, and days, because such conversion would be ambiguous. If you need such a conversion, you must implement it yourself, since the rules can depend on the start date and the calendar in use. -Negative values are never allowed as `Temporal.Duration` fields, so passing one as an argument to `new Temporal.Duration()` or as a property in the object passed to `Temporal.Duration.from()` will always throw an exception, regardless of the disambiguation mode. +`Temporal.Duration` fields are not allowed to have mixed signs. +For example, passing one positive and one negative argument to `new Temporal.Duration()` or as properties in the object passed to `Temporal.Duration.from()` will always throw an exception, regardless of the disambiguation mode. Therefore, the only case where constrain and reject mode have any effect when creating a duration, is integer overflow. -If one of the values overflows, constrain mode will cap it to `Number.MAX_VALUE`. +If one of the values overflows, constrain mode will cap it to `Number.MAX_VALUE` or `-Number.MAX_VALUE`. ## Duration arithmetic -When adding two `Temporal.Duration`s, the situation is much the same as for constructing one. -It's not possible to end up with a negative value when adding two valid durations, nor is any balancing needed, so the disambiguation mode only determines what to do in the case of integer overflow. +When adding two `Temporal.Duration`s of opposite sign, or subtracting two durations of the same sign, the situation is different. +As well as the same possibility of integer overflow, the fields may also need to be balanced. -The situation is very different for subtraction. Consider a duration of 90 minutes, from which is subtracted 30 seconds. Subtracting the fields directly would result in 90 minutes and −30 seconds, which is invalid. Therefore, it's necessary to balance the seconds with the minutes, resulting in 89 minutes and 30 seconds. -By default, the fields of the resulting duration are only converted between each other if one unit is negative but a larger unit is positive, in which case the smaller is balanced with the larger to avoid having negative-valued fields. +By default, the fields of the resulting duration are only converted between each other if one unit is negative but a larger unit is positive, or vice versa, in which case the smaller is balanced with the larger to avoid having mixed-sign fields. That's not all the balancing that _could_ be done on the resulting value. It could further be balanced into 1 hour, 29 minutes, and 30 seconds. However, that would likely conflict with the intention of having a duration of 90 minutes in the first place, so this should be behaviour that the Temporal user opts in to. -With subtraction, we make a distinction between "necessary balancing" and "optional balancing". +Here, we make a distinction between "necessary balancing" and "optional balancing". -In order to accommodate this, the `disambiguation` option when subtracting `Temporal.Duration`s is different from all the other arithmetic methods' disambiguation options. -Necessary balancing is called `balanceConstrain` mode, because values are constrained to be non-negative through balancing. +In order to accommodate this, the `disambiguation` option when performing arithmetic on `Temporal.Duration`s is different from all the other arithmetic methods' disambiguation options. +Necessary balancing is called `constrain` mode, because values are constrained to be non-negative through balancing. Optional balancing is called `balance` mode. -The usual `constrain` and `reject` modes are not available. +The usual `reject` mode is also available, and does the same thing as `constrain` but throws on integer overflow. -The default is `balanceConstrain` mode. +The default is `constrain` mode. The `balance` mode is only provided for convenience, since the following code snippets give the same result: ```javascript @@ -95,8 +95,8 @@ one.minus(two, { disambiguation: 'balance' }); // => PT1H45M // Result is negative one = Temporal.Duration.from({ hours: 2, minutes: 30 }); two = Temporal.Duration.from({ hours: 3 }); -one.minus(two); // throws -one.minus(two, { disambiguation: 'balance' }); // throws +one.minus(two); // -PT30M +one.minus(two, { disambiguation: 'balance' }); // -PT30M // Unbalanceable units, but also no balancing possible one = Temporal.Duration.from({ months: 3, days: 15 }); diff --git a/docs/date.md b/docs/date.md index ea92bc4a14..1eddea6615 100644 --- a/docs/date.md +++ b/docs/date.md @@ -323,6 +323,8 @@ For these cases, the `disambiguation` option tells what to do: Additionally, if the result is earlier or later than the range of dates that `Temporal.Date` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), then this method will throw a `RangeError` regardless of `disambiguation`. +Adding a negative duration is equivalent to subtracting the absolute value of that duration. + Usage example: ```javascript date = Temporal.Date.from('2006-08-24'); @@ -357,6 +359,8 @@ For these cases, the `disambiguation` option tells what to do: Additionally, if the result is earlier or later than the range of dates that `Temporal.Date` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), then this method will throw a `RangeError` regardless of `disambiguation`. +Subtracting a negative duration is equivalent to adding the absolute value of that duration. + Usage example: ```javascript date = Temporal.Date.from('2006-08-24'); diff --git a/docs/datetime.md b/docs/datetime.md index 388de1c5df..7366102690 100644 --- a/docs/datetime.md +++ b/docs/datetime.md @@ -387,6 +387,8 @@ For these cases, the `disambiguation` option tells what to do: Additionally, if the result is earlier or later than the range of dates that `Temporal.DateTime` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), then this method will throw a `RangeError` regardless of `disambiguation`. +Adding a negative duration is equivalent to subtracting the absolute value of that duration. + Usage example: ```javascript dt = new Temporal.DateTime(1995, 12, 7, 3, 24, 30, 0, 3, 500); @@ -421,6 +423,8 @@ For these cases, the `disambiguation` option tells what to do: Additionally, if the result is earlier or later than the range of dates that `Temporal.DateTime` can represent (approximately half a million years centered on the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)), then this method will throw a `RangeError` regardless of `disambiguation`. +Subtracting a negative duration is equivalent to adding the absolute value of that duration. + Usage example: ```javascript dt = new Temporal.DateTime(1995, 12, 7, 3, 24, 30, 0, 3, 500); diff --git a/docs/duration.md b/docs/duration.md index 85bff279d6..a7aae7604b 100644 --- a/docs/duration.md +++ b/docs/duration.md @@ -30,9 +30,9 @@ For more detailed information, see the ISO 8601 standard or the [Wikipedia page] | **PT0S** | Zero | | **P0D** | Zero | -> **NOTE:** According to the ISO 8601 standard, weeks are not allowed to appear together with any other units. -> As an extension to the standard, Temporal supports combining weeks with other units. -> If you intend to use a string such as **P3W1D** for interoperability, note that other programs may not accept it. +> **NOTE:** According to the ISO 8601 standard, weeks are not allowed to appear together with any other units, and durations can only be positive. +> As extensions to the standard, Temporal supports combining weeks with other units, and a single sign character at the start of the string. +> If you intend to use a string such as **P3W1D**, **+P1M**, or **-P1M** for interoperability, note that other programs may not accept it. ## Constructor @@ -53,10 +53,10 @@ For more detailed information, see the ISO 8601 standard or the [Wikipedia page] **Returns:** a new `Temporal.Duration` object. All of the arguments are optional. -Any missing or `undefined` numerical arguments are taken to be zero, and all non-integer numerical arguments are rounded down to the nearest integer. -Negative numbers are not allowed. +Any missing or `undefined` numerical arguments are taken to be zero, and all non-integer numerical arguments are rounded to the nearest integer, towards zero. +Any non-zero arguments must all have the same sign. -Use this constructor directly if you have the correct parameters already as numerical values, but otherwise `Temporal.Duration.from()`, which accepts more kinds of input and allows disambiguation behaviour, is probably more convenient. +Use this constructor directly if you have the correct parameters already as numerical values. Otherwise `Temporal.Duration.from()` is probably more convenient because it accepts more kinds of input and allows disambiguation behaviour. Usage examples: ```javascript @@ -88,7 +88,7 @@ Any missing ones will be assumed to be 0. Any non-object value is converted to a string, which is expected to be in ISO 8601 format. The `disambiguation` option controls how out-of-range values are interpreted: -- `constrain` (the default): Infinite values are clamped to `Number.MAX_VALUE`. +- `constrain` (the default): Infinite values are clamped to `Number.MAX_VALUE` or `-Number.MAX_VALUE`. Values higher than the next highest unit (for example, 90 minutes) are left as-is. - `balance`: Infinite values will cause the function to throw a `RangeError`. Values higher than the next highest unit, are converted to be in-range by incrementing the next highest unit accordingly. @@ -96,7 +96,8 @@ The `disambiguation` option controls how out-of-range values are interpreted: - `reject`: Infinite values will cause the function to throw a `RangeError`. Values higher than the next highest unit (for example, 90 minutes) are left as-is. -No matter which disambiguation mode is selected, negative values are never allowed and will cause the function to throw a `RangeError`. +No matter which disambiguation mode is selected, all non-zero values must have the same sign. +If they do not, the function will throw a `RangeError`. > **NOTE:** Years and months can have different lengths. In the default ISO calendar, a year can be 365 or 366 days, and a month can be 28, 29, 30, or 31 days. @@ -106,20 +107,23 @@ No conversion is ever performed between years, months, weeks, and days, even in > **NOTE:** This function understands strings where weeks and other units are combined, which are technically not valid ISO 8601 strings. > (For example, `P3W1D` is understood to mean three weeks and one day, although it is not valid according to ISO 8601.) +> **NOTE:** This function understands a single sign character at the start of a string, which is also technically not a valid ISO 8601 duration string. +> (For example, `-P1Y1M` is a negative duration of one year and one month, and `+P1Y1M` is one year and one month, although neither is a valid ISO 8601 duration string.) +> If no sign character is present, then the sign is assumed to be positive. + Usage examples: ```javascript d = Temporal.Duration.from({ years: 1, days: 1 }) // => P1Y1D -d = Temporal.Duration.from({ days: 2, hours: 12 }) // => P2DT12H +d = Temporal.Duration.from({ days: -2, hours: -12 }) // => -P2DT12H Temporal.Duration.from(d) === d // => true d = Temporal.Duration.from('P1Y1D') // => P1Y1D -d = Temporal.Duration.from('P2DT12H') // => P2DT12H +d = Temporal.Duration.from('-P2DT12H') // => -P2DT12H d = Temporal.Duration.from('P0D') // => PT0S -// Negative values are never allowed, even if overall positive: +// Mixed-sign values are never allowed, even if overall positive: d = Temporal.Duration.from({ hours: 1, minutes: -30 }) // throws -// FIXME https://github.com/tc39/proposal-temporal/issues/408 // Disambiguation @@ -167,6 +171,10 @@ d.microseconds // => 654 d.nanoseconds // => 321 ``` +### duration.**sign** : number + +The read-only `sign` property has the value –1, 0, or 1, depending on whether the duration is negative, zero, or positive. + ## Methods ### duration.**with**(_durationLike_: object, _options_?: object) : Temporal.Duration @@ -186,7 +194,7 @@ This method creates a new `Temporal.Duration` which is a copy of `duration`, but Since `Temporal.Duration` objects are immutable, use this method instead of modifying one. The `disambiguation` option specifies what to do with out-of-range or overly large values. -Negative numbers are never allowed as properties of `durationLike`. +All non-zero properties of `durationLike` must have the same sign, and they must additionally have the same sign as the non-zero properties of `duration`, unless they override all of these non-zero properties. If a property of `durationLike` is infinity, then constrain mode will clamp it to `Number.MAX_VALUE`. Reject and balance modes will throw a `RangeError` in that case. Additionally, balance mode will behave like it does in `Duration.from()` and perform a balance operation on the result. @@ -211,7 +219,7 @@ duration = duration.with({ years, months }, { disambiguation: 'balance' }); - `options` (optional object): An object with properties representing options for the addition. The following options are recognized: - `disambiguation` (string): How to deal with additions that result in out-of-range values. - Allowed values are `constrain` and `reject`. + Allowed values are `constrain`, `balance`, and `reject`. The default is `constrain`. **Returns:** a new `Temporal.Duration` object which represents the sum of the durations of `duration` and `other`. @@ -220,9 +228,16 @@ This method adds `other` to `duration`, resulting in a longer duration. The `other` argument is an object with properties denoting a duration, such as `{ hours: 5, minutes: 30 }`, or a `Temporal.Duration` object. -The `disambiguation` argument tells what to do in the case where the addition results in an out-of-range value: -- In `constrain` mode (the default), additions that result in a value too large to be represented in a Number are capped at `Number.MAX_VALUE`. +In order to be valid, the resulting duration must not have fields with mixed signs. +However, before the result is balanced, it's possible that the intermediate result will have one or more negative fields while the overall duration is positive, or vice versa. +For example, "4 hours and 15 minutes" minus "2 hours and 30 minutes" results in "2 hours and −15 minutes". +The `disambiguation` argument tells what to do in this case, or in the case where the addition results in an out-of-range value: +- In `constrain` mode (the default), additions that result in a value too large to be represented in a Number are capped at `Number.MAX_VALUE`, or `-Number.MAX_VALUE` if out of range in the other direction. + Additions resulting in mixed-sign fields will balance those fields with the next-highest field so that all the fields of the result have the same sign. +- In `balance` mode, if any addition results in a value too large to be represented in a Number, a `RangeError` is thrown. + As well, all fields are balanced with the next highest field, no matter if they have mixed signs or not. - In `reject` mode, if any addition results in a value too large to be represented in a Number, a `RangeError` is thrown. + Otherwise this is the same as `constrain`. The fields of the resulting duration are never converted between each other. If you need this behaviour, use `Duration.from()` with balance disambiguation, which will convert overly large units into the next highest unit, up to days. @@ -232,6 +247,8 @@ For usage examples and a more complete explanation of how balancing works and wh No conversion is ever performed between years, months, days, and other units, as that could be ambiguous depending on the start date. If you need such a conversion, you must implement it yourself, since the rules can depend on the start date and the calendar in use. +Adding a negative duration is equivalent to subtracting the absolute value of that duration. + Usage example: ```javascript hour = Temporal.Duration.from('PT1H'); @@ -283,16 +300,21 @@ The `other` argument is an object with properties denoting a duration, such as ` If `other` is larger than `duration` and the subtraction would result in a negative duration, the method will throw a `RangeError`. -In order to be valid, the resulting duration must not have any negative fields. -However, it's possible to have one or more of the fields be negative while the overall duration is still positive. +In order to be valid, the resulting duration must not have fields with mixed signs. +However, before the result is balanced, it's possible that the intermediate result will have one or more negative fields while the overall duration is positive, or vice versa. For example, "4 hours and 15 minutes" minus "2 hours and 30 minutes" results in "2 hours and −15 minutes". -The `disambiguation` option tells what to do in this case. -- In `balanceConstrain` mode (the default), negative fields are balanced with the next highest field so that none of the fields are negative in the result. - If this is not possible, a `RangeError` is thrown. -- In `balance` mode, all fields are balanced with the next highest field, no matter if they are negative or not. +The `disambiguation` argument tells what to do in this case: +- In `constrain` mode (the default), subtractions that result in a value too large to be represented in a Number are capped at `Number.MAX_VALUE`, or `-Number.MAX_VALUE` if out of range in the other direction. + Subtractions resulting in mixed-sign fields will balance those fields with the next-highest field so that all the fields of the result have the same sign. +- In `balance` mode, if any subtraction results in a value too large to be represented in a Number, a `RangeError` is thrown. + As well, all fields are balanced with the next highest field, no matter if they have mixed signs or not. +- In `reject` mode, if any subtraction results in a value too large to be represented in a Number, a `RangeError` is thrown. + Otherwise this is the same as `constrain`. For usage examples and a more complete explanation of how balancing works and why it is necessary, especially for subtracting `Temporal.Duration`, see [Duration balancing](./balancing.md#duration-arithmetic). +Subtracting a negative duration is equivalent to adding the absolute value of that duration. + Usage example: ```javascript hourAndAHalf = Temporal.Duration.from('PT1H30M'); @@ -306,7 +328,7 @@ one.minus(two, { disambiguation: 'balance' }); // => PT2H59M30S // Example of not balancing: threeYears = Temporal.Duration.from({ years: 3 }); oneAndAHalfYear = Temporal.Duration.from({ years: 1, months: 6 }); -threeYears.minus(oneAndAHalfYear) // throws; months are negative and cannot be balanced +threeYears.minus(oneAndAHalfYear) // throws; mixed months and years signs cannot be balanced // Example of a custom conversion using ISO calendar rules: function yearsToMonths(duration) { let { years, months } = duration; @@ -316,6 +338,36 @@ function yearsToMonths(duration) { yearsToMonths(threeYears).minus(yearsToMonths(oneAndAHalfYear)) // => P18M ``` +### duration.**negated**() : Temporal.Duration + +**Returns:** a new `Temporal.Duration` object with the opposite sign. + +This method gives the negation of `duration`. +It returns a newly constructed `Temporal.Duration` with all the fields having the opposite sign (positive if negative, and vice versa.) +If `duration` is zero, then the returned object is a copy of `duration`. + +Usage example: +```javascript +d = Temporal.Duration.from('P1Y2M3DT4H5M6.987654321S'); +d.sign // 1 +d.negated() // -P1Y2M3DT4H5M6.987654321S +d.negated().sign // -1 +``` + +### duration.**abs**() : Temporal.Duration + +**Returns:** a new `Temporal.Duration` object that is always positive. + +This method gives the absolute value of `duration`. +It returns a newly constructed `Temporal.Duration` with all the fields having the same magnitude as those of `duration`, but positive. +If `duration` is already positive or zero, then the returned object is a copy of `duration`. + +Usage example: +```javascript +d = Temporal.Duration.from('-PT8H30M'); +d.abs() // PT8H30M +``` + ### duration.**getFields**() : { years: number, months: number, weeks: number, days: number, hours: number, minutes: number, seconds: number, milliseconds: number, microseconds: number, nanoseconds: number } **Returns:** a plain object with properties equal to the fields of `duration`. @@ -342,10 +394,15 @@ This method overrides `Object.prototype.toString()` and provides the ISO 8601 de > **NOTE**: The output of `duration.toString()` may combine weeks with other units, which is technically invalid according to ISO 8601. > (For example, `P3W` for three weeks is valid, but `P3W1D` for three weeks and one day is not.) +> **NOTE**: Negative durations do not exist in ISO 8601, so the result of calling `toString()` on a negative duration is technically not a valid ISO 8601 string. +> Serializing negative durations with a leading minus sign is an extension to ISO 8601 that is accepted by most date/time libraries in JavaScript and other languages. + Usage examples: ```javascript d = Temporal.Duration.from({ years: 1, days: 1 }); d.toString(); // => P1Y1D +d = Temporal.Duration.from({ years: -1, days: -1 }); +d.toString(); // => -P1Y1D d = Temporal.Duration.from({ milliseconds: 1000 }); d.toString(); // => PT1S diff --git a/docs/iso-string-ext.md b/docs/iso-string-ext.md index 0bd33962fc..dcd82ca0ab 100644 --- a/docs/iso-string-ext.md +++ b/docs/iso-string-ext.md @@ -95,3 +95,8 @@ Since it is ambiguous whether that string represents a Date, YearMonth, or Month The ISO 8601 standard allows durations in units of weeks (`P3W` for three weeks), but weeks are not allowed to appear together with any other units. Temporal.Duration does support combining weeks with other units, so we propose the convention of strings such as `P3W1DT1H` for three weeks, one day, and one hour. + +## Duration signs + +Other popular datetime libraries, such as Moment.js, allow a single sign character (`+`, `-`, or the Unicode minus sign U+2122) to be prepended to an ISO 8601 duration string, to allow for negative durations. +This is also supported in Temporal.Duration. diff --git a/docs/time.md b/docs/time.md index d4f8fbf1ec..0e82757ef3 100644 --- a/docs/time.md +++ b/docs/time.md @@ -200,6 +200,8 @@ The `duration` argument is an object with properties denoting a duration, such a The `disambiguation` parameter has no effect in the default ISO calendar, because the units of hours, minutes, and seconds are always the same length and therefore not ambiguous. However, it may have an effect in other calendars where those units are not always the same length. +Adding a negative duration is equivalent to subtracting the absolute value of that duration. + Usage example: ```javascript time = Temporal.Time.from('19:39:09.068346205'); @@ -226,6 +228,8 @@ The `duration` argument is an object with properties denoting a duration, such a The `disambiguation` parameter has no effect in the default ISO calendar, because the units of hours, minutes, and seconds are always the same length and therefore not ambiguous. However, it may have an effect in other calendars where those units are not always the same length. +Subtracting a negative duration is equivalent to adding the absolute value of that duration. + Usage example: ```javascript time = Temporal.Time.from('19:39:09.068346205'); diff --git a/docs/yearmonth.md b/docs/yearmonth.md index 244ddc9fc4..aa5390521e 100644 --- a/docs/yearmonth.md +++ b/docs/yearmonth.md @@ -251,6 +251,8 @@ Other than for out-of-range values, the `disambiguation` option has no effect in It doesn't matter in this case that years and months can be different numbers of days, as the resolution of `Temporal.YearMonth` does not distinguish days. However, disambiguation may have an effect in other calendars where years can be different numbers of months. +Adding a negative duration is equivalent to subtracting the absolute value of that duration. + Usage example: ```javascript ym = Temporal.YearMonth.from('2019-06'); @@ -279,6 +281,8 @@ Other than for out-of-range values, the `disambiguation` option has no effect in It doesn't matter in this case that years and months can be different numbers of days, as the resolution of `Temporal.YearMonth` does not distinguish days. However, disambiguation may have an effect in other calendars where years can be different numbers of months. +Subtracting a negative duration is equivalent to adding the absolute value of that duration. + Usage example: ```javascript ym = Temporal.YearMonth.from('2019-06'); diff --git a/polyfill/index.d.ts b/polyfill/index.d.ts index de0bf4160c..a80d2691ad 100644 --- a/polyfill/index.d.ts +++ b/polyfill/index.d.ts @@ -22,9 +22,10 @@ export namespace Temporal { /** * Options for assigning fields using `Duration.prototype.with()` or entire - * objects with `Duration.prototype.from()`. + * objects with `Duration.prototype.from()`, and for arithmetic with + * `Duration.prototype.plus()` and `Duration.prototype.minus()`. * */ - export type DurationAssignmentOptions = { + export type DurationOptions = { /** * How to deal with out-of-range values * @@ -85,25 +86,6 @@ export namespace Temporal { disambiguation: 'constrain' | 'reject'; }; - /** - * Options to control `Duration.prototype.minus()` behavior - * */ - export type DurationMinusOptions = { - /** - * Controls how to deal with subtractions that result in negative overflows - * - * In `'balanceConstrain'` mode, negative fields are balanced with the next - * highest field so that none of the fields are negative in the result. If - * this is not possible, a `RangeError` is thrown. - * - * In `'balance'` mode, all fields are balanced with the next highest field, - * no matter if they are negative or not. - * - * The default is `'balanceConstrain'`. - */ - disambiguation: 'balanceConstrain' | 'balance'; - }; - export interface DifferenceOptions { /** * The largest unit to allow in the resulting `Temporal.Duration` object. @@ -141,10 +123,7 @@ export namespace Temporal { * See https://tc39.es/proposal-temporal/docs/duration.html for more details. */ export class Duration implements DurationFields { - static from( - item: Temporal.Duration | DurationLike | string, - options?: DurationAssignmentOptions - ): Temporal.Duration; + static from(item: Temporal.Duration | DurationLike | string, options?: DurationOptions): Temporal.Duration; constructor( years?: number, months?: number, @@ -157,6 +136,7 @@ export namespace Temporal { microseconds?: number, nanoseconds?: number ); + readonly sign: -1 | 0 | 1; readonly years: number; readonly months: number; readonly weeks: number; @@ -167,9 +147,11 @@ export namespace Temporal { readonly milliseconds: number; readonly microseconds: number; readonly nanoseconds: number; - with(durationLike: DurationLike, options?: DurationAssignmentOptions): Temporal.Duration; - plus(other: Temporal.Duration | DurationLike, options?: ArithmeticOptions): Temporal.Duration; - minus(other: Temporal.Duration | DurationLike, options?: DurationMinusOptions): Temporal.Duration; + negated(): Temporal.Duration; + abs(): Temporal.Duration; + with(durationLike: DurationLike, options?: DurationOptions): Temporal.Duration; + plus(other: Temporal.Duration | DurationLike, options?: DurationOptions): Temporal.Duration; + minus(other: Temporal.Duration | DurationLike, options?: DurationOptions): Temporal.Duration; getFields(): DurationFields; toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; toJSON(): string; diff --git a/polyfill/lib/absolute.mjs b/polyfill/lib/absolute.mjs index 7ef80930c5..ab3168e7ed 100644 --- a/polyfill/lib/absolute.mjs +++ b/polyfill/lib/absolute.mjs @@ -55,6 +55,7 @@ export class Absolute { microseconds, nanoseconds } = ES.ToLimitedTemporalDuration(temporalDurationLike, ['years', 'months', 'weeks']); + ES.RejectDurationSign(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); let add = bigInt(0); add = add.plus(bigInt(nanoseconds)); @@ -84,6 +85,7 @@ export class Absolute { microseconds, nanoseconds } = ES.ToLimitedTemporalDuration(temporalDurationLike, ['years', 'months', 'weeks']); + ES.RejectDurationSign(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); let add = bigInt(0); add = add.plus(bigInt(nanoseconds)); diff --git a/polyfill/lib/calendar.mjs b/polyfill/lib/calendar.mjs index a1db5372be..c36db75ad4 100644 --- a/polyfill/lib/calendar.mjs +++ b/polyfill/lib/calendar.mjs @@ -147,20 +147,32 @@ class ISO8601 extends Calendar { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); const disambiguation = ES.ToTemporalDisambiguation(options); const { years, months, weeks, days } = duration; + ES.RejectDurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); + const sign = ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); let year = GetSlot(date, ISO_YEAR); let month = GetSlot(date, ISO_MONTH); let day = GetSlot(date, ISO_DAY); - ({ year, month, day } = ES.AddDate(year, month, day, years, months, weeks, days, disambiguation)); + if (sign < 0) { + ({ year, month, day } = ES.SubtractDate(year, month, day, -years, -months, -weeks, -days, disambiguation)); + } else { + ({ year, month, day } = ES.AddDate(year, month, day, years, months, weeks, days, disambiguation)); + } return new constructor(year, month, day, this); } dateMinus(date, duration, options, constructor) { if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver'); const disambiguation = ES.ToTemporalDisambiguation(options); const { years, months, weeks, days } = duration; + ES.RejectDurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); + const sign = ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); let year = GetSlot(date, ISO_YEAR); let month = GetSlot(date, ISO_MONTH); let day = GetSlot(date, ISO_DAY); - ({ year, month, day } = ES.SubtractDate(year, month, day, years, months, weeks, days, disambiguation)); + if (sign < 0) { + ({ year, month, day } = ES.AddDate(year, month, day, -years, -months, -weeks, -days, disambiguation)); + } else { + ({ year, month, day } = ES.SubtractDate(year, month, day, years, months, weeks, days, disambiguation)); + } return new constructor(year, month, day, this); } dateDifference(smaller, larger, options) { diff --git a/polyfill/lib/date.mjs b/polyfill/lib/date.mjs index 7a8d2b4805..e53072e979 100644 --- a/polyfill/lib/date.mjs +++ b/polyfill/lib/date.mjs @@ -124,17 +124,9 @@ export class Date { plus(temporalDurationLike, options) { if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver'); let duration = ES.ToLimitedTemporalDuration(temporalDurationLike); - const { years, months, weeks, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - const { days } = ES.BalanceDuration( - duration.days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - 'days' - ); + let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + ({ days } = ES.BalanceDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 'days')); duration = { years, months, weeks, days }; const Construct = ES.SpeciesConstructor(this, Date); const result = GetSlot(this, CALENDAR).datePlus(this, duration, options, Construct); @@ -144,17 +136,9 @@ export class Date { minus(temporalDurationLike, options) { if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver'); let duration = ES.ToLimitedTemporalDuration(temporalDurationLike); - const { years, months, weeks, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - const { days } = ES.BalanceDuration( - duration.days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - 'days' - ); + let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + ({ days } = ES.BalanceDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 'days')); duration = { years, months, weeks, days }; const Construct = ES.SpeciesConstructor(this, Date); const result = GetSlot(this, CALENDAR).dateMinus(this, duration, options, Construct); diff --git a/polyfill/lib/datetime.mjs b/polyfill/lib/datetime.mjs index e00193561c..6a63a491d1 100644 --- a/polyfill/lib/datetime.mjs +++ b/polyfill/lib/datetime.mjs @@ -233,6 +233,9 @@ export class DateTime { if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver'); let duration = ES.ToLimitedTemporalDuration(temporalDurationLike); let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + // For a negative duration, BalanceDuration() subtracts from days to make + // all other units positive, so it's not needed to switch on the sign below ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration( days, hours, @@ -298,6 +301,9 @@ export class DateTime { if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver'); let duration = ES.ToLimitedTemporalDuration(temporalDurationLike); let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + // For a negative duration, BalanceDuration() subtracts from days to make + // all other units positive, so it's not needed to switch on the sign below ({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceDuration( days, hours, diff --git a/polyfill/lib/duration.mjs b/polyfill/lib/duration.mjs index 42802dd96c..be58a1a515 100644 --- a/polyfill/lib/duration.mjs +++ b/polyfill/lib/duration.mjs @@ -42,9 +42,22 @@ export class Duration { microseconds = ES.ToInteger(microseconds); nanoseconds = ES.ToInteger(nanoseconds); + const sign = ES.DurationSign( + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds + ); for (const prop of [years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds]) { - if (prop < 0) throw new RangeError('negative values not allowed as duration fields'); if (!Number.isFinite(prop)) throw new RangeError('infinite values not allowed as duration fields'); + const propSign = Math.sign(prop); + if (propSign !== 0 && propSign !== sign) throw new RangeError('mixed-sign values not allowed as duration fields'); } CreateSlots(this); @@ -108,6 +121,21 @@ export class Duration { if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); return GetSlot(this, NANOSECONDS); } + get sign() { + if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); + return ES.DurationSign( + GetSlot(this, YEARS), + GetSlot(this, MONTHS), + GetSlot(this, WEEKS), + GetSlot(this, DAYS), + GetSlot(this, HOURS), + GetSlot(this, MINUTES), + GetSlot(this, SECONDS), + GetSlot(this, MILLISECONDS), + GetSlot(this, MICROSECONDS), + GetSlot(this, NANOSECONDS) + ); + } with(durationLike, options) { if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); const disambiguation = ES.ToDurationTemporalDisambiguation(options); @@ -178,6 +206,42 @@ export class Duration { if (!ES.IsTemporalDuration(result)) throw new TypeError('invalid result'); return result; } + negated() { + if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); + const Construct = ES.SpeciesConstructor(this, Duration); + const result = new Construct( + -GetSlot(this, YEARS), + -GetSlot(this, MONTHS), + -GetSlot(this, WEEKS), + -GetSlot(this, DAYS), + -GetSlot(this, HOURS), + -GetSlot(this, MINUTES), + -GetSlot(this, SECONDS), + -GetSlot(this, MILLISECONDS), + -GetSlot(this, MICROSECONDS), + -GetSlot(this, NANOSECONDS) + ); + if (!ES.IsTemporalDuration(result)) throw new TypeError('invalid result'); + return result; + } + abs() { + if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); + const Construct = ES.SpeciesConstructor(this, Duration); + const result = new Construct( + Math.abs(GetSlot(this, YEARS)), + Math.abs(GetSlot(this, MONTHS)), + Math.abs(GetSlot(this, WEEKS)), + Math.abs(GetSlot(this, DAYS)), + Math.abs(GetSlot(this, HOURS)), + Math.abs(GetSlot(this, MINUTES)), + Math.abs(GetSlot(this, SECONDS)), + Math.abs(GetSlot(this, MILLISECONDS)), + Math.abs(GetSlot(this, MICROSECONDS)), + Math.abs(GetSlot(this, NANOSECONDS)) + ); + if (!ES.IsTemporalDuration(result)) throw new TypeError('invalid result'); + return result; + } plus(other, options) { if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver'); let { @@ -192,8 +256,20 @@ export class Duration { microseconds, nanoseconds } = ES.ToLimitedTemporalDuration(other); - const disambiguation = ES.ToTemporalDisambiguation(options); - ({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.AddDuration( + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + const disambiguation = ES.ToDurationTemporalDisambiguation(options); + ({ + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds + } = ES.DurationArithmetic( GetSlot(this, YEARS), GetSlot(this, MONTHS), GetSlot(this, WEEKS), @@ -246,7 +322,8 @@ export class Duration { microseconds, nanoseconds } = ES.ToLimitedTemporalDuration(other); - const disambiguation = ES.ToDurationSubtractionTemporalDisambiguation(options); + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + const disambiguation = ES.ToDurationTemporalDisambiguation(options); ({ years, months, @@ -258,7 +335,7 @@ export class Duration { milliseconds, microseconds, nanoseconds - } = ES.SubtractDuration( + } = ES.DurationArithmetic( GetSlot(this, YEARS), GetSlot(this, MONTHS), GetSlot(this, WEEKS), @@ -269,16 +346,16 @@ export class Duration { GetSlot(this, MILLISECONDS), GetSlot(this, MICROSECONDS), GetSlot(this, NANOSECONDS), - years, - months, - weeks, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + -years, + -months, + -weeks, + -days, + -hours, + -minutes, + -seconds, + -milliseconds, + -microseconds, + -nanoseconds, disambiguation )); const Construct = ES.SpeciesConstructor(this, Duration); diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index b005418916..f6604148e5 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -210,20 +210,21 @@ export const ES = ObjectAssign({}, ES2019, { ParseTemporalDurationString: (isoString) => { const match = PARSE.duration.exec(isoString); if (!match) throw new RangeError(`invalid duration: ${isoString}`); - if (match.slice(1).every((element) => element === undefined)) { + if (match.slice(2).every((element) => element === undefined)) { throw new RangeError(`invalid duration: ${isoString}`); } - const years = ES.ToInteger(match[1]); - const months = ES.ToInteger(match[2]); - const weeks = ES.ToInteger(match[3]); - const days = ES.ToInteger(match[4]); - const hours = ES.ToInteger(match[5]); - const minutes = ES.ToInteger(match[6]); - const seconds = ES.ToInteger(match[7]); - const fraction = match[8] + '000000000'; - const milliseconds = ES.ToInteger(fraction.slice(0, 3)); - const microseconds = ES.ToInteger(fraction.slice(3, 6)); - const nanoseconds = ES.ToInteger(fraction.slice(6, 9)); + const sign = match[1] === '-' ? -1 : 1; + const years = ES.ToInteger(match[2]) * sign; + const months = ES.ToInteger(match[3]) * sign; + const weeks = ES.ToInteger(match[4]) * sign; + const days = ES.ToInteger(match[5]) * sign; + const hours = ES.ToInteger(match[6]) * sign; + const minutes = ES.ToInteger(match[7]) * sign; + const seconds = ES.ToInteger(match[8]) * sign; + const fraction = match[9] + '000000000'; + const milliseconds = ES.ToInteger(fraction.slice(0, 3)) * sign; + const microseconds = ES.ToInteger(fraction.slice(3, 6)) * sign; + const nanoseconds = ES.ToInteger(fraction.slice(6, 9)) * sign; return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; }, ParseTemporalAbsolute: (isoString) => { @@ -369,10 +370,7 @@ export const ES = ObjectAssign({}, ES2019, { nanoseconds, disambiguation ) => { - for (const prop of [years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds]) { - if (prop < 0) throw new RangeError('negative values not allowed as duration fields'); - } - + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); switch (disambiguation) { case 'reject': for (const prop of [ @@ -393,7 +391,7 @@ export const ES = ObjectAssign({}, ES2019, { case 'constrain': { const arr = [years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds]; for (const idx in arr) { - if (!Number.isFinite(arr[idx])) arr[idx] = Number.MAX_VALUE; + if (!Number.isFinite(arr[idx])) arr[idx] = Math.sign(arr[idx]) * Number.MAX_VALUE; } [years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds] = arr; break; @@ -474,9 +472,6 @@ export const ES = ObjectAssign({}, ES2019, { ToTimeZoneTemporalDisambiguation: (options) => { return ES.GetOption(options, 'disambiguation', ['compatible', 'earlier', 'later', 'reject'], 'compatible'); }, - ToDurationSubtractionTemporalDisambiguation: (options) => { - return ES.GetOption(options, 'disambiguation', ['balanceConstrain', 'balance'], 'balanceConstrain'); - }, ToLargestTemporalUnit: (options, fallback, disallowedStrings = []) => { const allowed = new Set([ 'years', @@ -727,32 +722,42 @@ export const ES = ObjectAssign({}, ES2019, { if (num <= Number.MAX_SAFE_INTEGER) return num.toString(10); return bigInt(num).toString(); } + + const years = GetSlot(duration, YEARS); + const months = GetSlot(duration, MONTHS); + const weeks = GetSlot(duration, WEEKS); + const days = GetSlot(duration, DAYS); + const hours = GetSlot(duration, HOURS); + const minutes = GetSlot(duration, MINUTES); + const seconds = GetSlot(duration, SECONDS); + let ms = GetSlot(duration, MILLISECONDS); + let µs = GetSlot(duration, MICROSECONDS); + let ns = GetSlot(duration, NANOSECONDS); + const sign = ES.DurationSign(years, months, weeks, days, hours, minutes, seconds, ms, µs, ns); + const dateParts = []; - if (GetSlot(duration, YEARS)) dateParts.push(`${formatNumber(GetSlot(duration, YEARS))}Y`); - if (GetSlot(duration, MONTHS)) dateParts.push(`${formatNumber(GetSlot(duration, MONTHS))}M`); - if (GetSlot(duration, WEEKS)) dateParts.push(`${formatNumber(GetSlot(duration, WEEKS))}W`); - if (GetSlot(duration, DAYS)) dateParts.push(`${formatNumber(GetSlot(duration, DAYS))}D`); + if (years) dateParts.push(`${formatNumber(Math.abs(years))}Y`); + if (months) dateParts.push(`${formatNumber(Math.abs(months))}M`); + if (weeks) dateParts.push(`${formatNumber(Math.abs(weeks))}W`); + if (days) dateParts.push(`${formatNumber(Math.abs(days))}D`); const timeParts = []; - if (GetSlot(duration, HOURS)) timeParts.push(`${formatNumber(GetSlot(duration, HOURS))}H`); - if (GetSlot(duration, MINUTES)) timeParts.push(`${formatNumber(GetSlot(duration, MINUTES))}M`); + if (hours) timeParts.push(`${formatNumber(Math.abs(hours))}H`); + if (minutes) timeParts.push(`${formatNumber(Math.abs(minutes))}M`); const secondParts = []; - let ms = GetSlot(duration, MILLISECONDS); - let µs = GetSlot(duration, MICROSECONDS); - let ns = GetSlot(duration, NANOSECONDS); - let seconds; - ({ seconds, millisecond: ms, microsecond: µs, nanosecond: ns } = ES.BalanceSubSecond(ms, µs, ns)); - const s = GetSlot(duration, SECONDS) + seconds; - if (ns) secondParts.unshift(`${ns}`.padStart(3, '0')); - if (µs || secondParts.length) secondParts.unshift(`${µs}`.padStart(3, '0')); - if (ms || secondParts.length) secondParts.unshift(`${ms}`.padStart(3, '0')); + let s; + ({ seconds: s, millisecond: ms, microsecond: µs, nanosecond: ns } = ES.BalanceSubSecond(ms, µs, ns)); + s += seconds; + if (ns) secondParts.unshift(`${Math.abs(ns)}`.padStart(3, '0')); + if (µs || secondParts.length) secondParts.unshift(`${Math.abs(µs)}`.padStart(3, '0')); + if (ms || secondParts.length) secondParts.unshift(`${Math.abs(ms)}`.padStart(3, '0')); if (secondParts.length) secondParts.unshift('.'); - if (s || secondParts.length) secondParts.unshift(formatNumber(s)); + if (s || secondParts.length) secondParts.unshift(formatNumber(Math.abs(s))); if (secondParts.length) timeParts.push(`${secondParts.join('')}S`); if (timeParts.length) timeParts.unshift('T'); if (!dateParts.length && !timeParts.length) return 'PT0S'; - return `P${dateParts.join('')}${timeParts.join('')}`; + return `${sign < 0 ? '-' : ''}P${dateParts.join('')}${timeParts.join('')}`; }, GetCanonicalTimeZoneIdentifier: (timeZoneIdentifier) => { @@ -1000,6 +1005,12 @@ export const ES = ObjectAssign({}, ES2019, { return week; }, + DurationSign: (y, mon, w, d, h, min, s, ms, µs, ns) => { + for (const prop of [y, mon, w, d, h, min, s, ms, µs, ns]) { + if (prop !== 0) return prop < 0 ? -1 : 1; + } + return 0; + }, BalanceYearMonth: (year, month) => { if (!Number.isFinite(year) || !Number.isFinite(month)) throw new RangeError('infinity is out of range'); @@ -1233,6 +1244,13 @@ export const ES = ObjectAssign({}, ES2019, { ES.RejectToRange(month, 1, 9); } }, + RejectDurationSign: (y, mon, w, d, h, min, s, ms, µs, ns) => { + const sign = ES.DurationSign(y, mon, w, d, h, min, s, ms, µs, ns); + for (const prop of [y, mon, w, d, h, min, s, ms, µs, ns]) { + const propSign = Math.sign(prop); + if (propSign !== 0 && propSign !== sign) throw new RangeError('mixed-sign values not allowed as duration fields'); + } + }, DifferenceDate: (smaller, larger, largestUnit = 'days') => { let years = larger.year - smaller.year; @@ -1390,7 +1408,7 @@ export const ES = ObjectAssign({}, ES2019, { )); return { deltaDays, hour, minute, second, millisecond, microsecond, nanosecond }; }, - AddDuration: ( + DurationArithmetic: ( y1, mon1, w1, @@ -1424,7 +1442,7 @@ export const ES = ObjectAssign({}, ES2019, { let microseconds = µs1 + µs2; let nanoseconds = ns1 + ns2; - return ES.RegulateDuration( + const sign = ES.DurationSign( years, months, weeks, @@ -1434,43 +1452,18 @@ export const ES = ObjectAssign({}, ES2019, { seconds, milliseconds, microseconds, - nanoseconds, - disambiguation + nanoseconds ); - }, - SubtractDuration: ( - y1, - mon1, - w1, - d1, - h1, - min1, - s1, - ms1, - µs1, - ns1, - y2, - mon2, - w2, - d2, - h2, - min2, - s2, - ms2, - µs2, - ns2, - disambiguation - ) => { - let years = y1 - y2; - let months = mon1 - mon2; - let weeks = w1 - w2; - let days = d1 - d2; - let hours = h1 - h2; - let minutes = min1 - min2; - let seconds = s1 - s2; - let milliseconds = ms1 - ms2; - let microseconds = µs1 - µs2; - let nanoseconds = ns1 - ns2; + years *= sign; + months *= sign; + weeks *= sign; + days *= sign; + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; if (nanoseconds < 0) { microseconds += Math.floor(nanoseconds / 1000); @@ -1497,27 +1490,34 @@ export const ES = ObjectAssign({}, ES2019, { hours = ES.NonNegativeModulo(hours, 24); } - for (const prop of [years, months, weeks, days]) { - if (prop < 0) throw new RangeError('negative values not allowed as duration fields'); + for (const prop of [months, weeks, days]) { + if (prop < 0) throw new RangeError('mixed sign not allowed in duration fields'); } - if (disambiguation === 'balance') { - return ES.RegulateDuration( - years, - months, - weeks, - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - 'balance' - ); - } + years *= sign; + months *= sign; + weeks *= sign; + days *= sign; + hours *= sign; + minutes *= sign; + seconds *= sign; + milliseconds *= sign; + microseconds *= sign; + nanoseconds *= sign; - return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds }; + return ES.RegulateDuration( + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + disambiguation + ); }, AssertPositiveInteger: (num) => { diff --git a/polyfill/lib/regex.mjs b/polyfill/lib/regex.mjs index 94fe383531..82f6365224 100644 --- a/polyfill/lib/regex.mjs +++ b/polyfill/lib/regex.mjs @@ -24,4 +24,4 @@ export const time = new RegExp(`^${timesplit.source}(?:${zonesplit.source})?(?:$ export const yearmonth = new RegExp(`^(${yearpart.source})-?(\\d{2})$`); export const monthday = /^(?:--)?(\d{2})-?(\d{2})$/; -export const duration = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?!$)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:[.,](\d{1,9}))?S)?)?$/i; +export const duration = /^([+-])?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?!$)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:[.,](\d{1,9}))?S)?)?$/i; diff --git a/polyfill/lib/time.mjs b/polyfill/lib/time.mjs index 27751a3b5c..8136768e91 100644 --- a/polyfill/lib/time.mjs +++ b/polyfill/lib/time.mjs @@ -113,21 +113,51 @@ export class Time { let { hour, minute, second, millisecond, microsecond, nanosecond } = this; const duration = ES.ToLimitedTemporalDuration(temporalDurationLike); const disambiguation = ES.ToTemporalDisambiguation(options); - const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.AddTime( - hour, - minute, - second, - millisecond, - microsecond, - nanosecond, + const { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + const sign = ES.DurationSign( + years, + months, + weeks, + days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - )); + ); + if (sign < 0) { + ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.SubtractTime( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + -hours, + -minutes, + -seconds, + -milliseconds, + -microseconds, + -nanoseconds + )); + } else { + ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.AddTime( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds + )); + } ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.RegulateTime( hour, minute, @@ -147,21 +177,51 @@ export class Time { let { hour, minute, second, millisecond, microsecond, nanosecond } = this; const duration = ES.ToLimitedTemporalDuration(temporalDurationLike); const disambiguation = ES.ToTemporalDisambiguation(options); - const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.SubtractTime( - hour, - minute, - second, - millisecond, - microsecond, - nanosecond, + const { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + const sign = ES.DurationSign( + years, + months, + weeks, + days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds - )); + ); + if (sign < 0) { + ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.AddTime( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + -hours, + -minutes, + -seconds, + -milliseconds, + -microseconds, + -nanoseconds + )); + } else { + ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.SubtractTime( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds + )); + } ({ hour, minute, second, millisecond, microsecond, nanosecond } = ES.RegulateTime( hour, minute, diff --git a/polyfill/lib/yearmonth.mjs b/polyfill/lib/yearmonth.mjs index 9e08aaf181..6c4b847464 100644 --- a/polyfill/lib/yearmonth.mjs +++ b/polyfill/lib/yearmonth.mjs @@ -78,23 +78,17 @@ export class YearMonth { plus(temporalDurationLike, options) { if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver'); const duration = ES.ToLimitedTemporalDuration(temporalDurationLike); - const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - const { days } = ES.BalanceDuration( - duration.days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - 'days' - ); + let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + ({ days } = ES.BalanceDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 'days')); const TemporalDate = GetIntrinsic('%Temporal.Date%'); const calendar = GetSlot(this, CALENDAR); const fields = ES.ToTemporalYearMonthRecord(this); - const firstOfCalendarMonth = calendar.dateFromFields({ ...fields, day: 1 }, {}, TemporalDate); - const addedDate = calendar.datePlus(firstOfCalendarMonth, { ...duration, days }, options, TemporalDate); + const sign = ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); + const day = sign < 0 ? calendar.daysInMonth(this) : 1; + const startDate = calendar.dateFromFields({ ...fields, day }, {}, TemporalDate); + const addedDate = calendar.datePlus(startDate, { ...duration, days }, options, TemporalDate); const Construct = ES.SpeciesConstructor(this, YearMonth); const result = calendar.yearMonthFromFields(addedDate, options, Construct); @@ -104,24 +98,17 @@ export class YearMonth { minus(temporalDurationLike, options) { if (!ES.IsTemporalYearMonth(this)) throw new TypeError('invalid receiver'); const duration = ES.ToLimitedTemporalDuration(temporalDurationLike); - const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; - const { days } = ES.BalanceDuration( - duration.days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - 'days' - ); + let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = duration; + ES.RejectDurationSign(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); + ({ days } = ES.BalanceDuration(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 'days')); const TemporalDate = GetIntrinsic('%Temporal.Date%'); const calendar = GetSlot(this, CALENDAR); const fields = ES.ToTemporalYearMonthRecord(this); - const lastDay = calendar.daysInMonth(this); - const lastOfCalendarMonth = calendar.dateFromFields({ ...fields, day: lastDay }, {}, TemporalDate); - const subtractedDate = calendar.dateMinus(lastOfCalendarMonth, { ...duration, days }, options, TemporalDate); + const sign = ES.DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0); + const day = sign < 0 ? 1 : calendar.daysInMonth(this); + const startDate = calendar.dateFromFields({ ...fields, day }, {}, TemporalDate); + const subtractedDate = calendar.dateMinus(startDate, { ...duration, days }, options, TemporalDate); const Construct = ES.SpeciesConstructor(this, YearMonth); const result = calendar.yearMonthFromFields(subtractedDate, options, Construct); diff --git a/polyfill/test/Duration/constructor/from/negative-infinity-handled.js b/polyfill/test/Duration/constructor/from/negative-infinity-handled.js index f6f9830e74..0103b3b66c 100644 --- a/polyfill/test/Duration/constructor/from/negative-infinity-handled.js +++ b/polyfill/test/Duration/constructor/from/negative-infinity-handled.js @@ -8,16 +8,116 @@ esid: sec-temporal.duration.from // constrain -assert.throws(RangeError, () => Temporal.Duration.from({ years: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ months: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ weeks: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ days: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ hours: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ minutes: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ seconds: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ milliseconds: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ microseconds: -Infinity }, { disambiguation: 'constrain' })); -assert.throws(RangeError, () => Temporal.Duration.from({ nanoseconds: -Infinity }, { disambiguation: 'constrain' })); +let result = Temporal.Duration.from({ years: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, -Number.MAX_VALUE); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ months: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, -Number.MAX_VALUE); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ weeks: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, -Number.MAX_VALUE); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ days: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, -Number.MAX_VALUE); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ hours: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, -Number.MAX_VALUE); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ minutes: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, -Number.MAX_VALUE); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ seconds: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, -Number.MAX_VALUE); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ milliseconds: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, -Number.MAX_VALUE); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ microseconds: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, -Number.MAX_VALUE); +assert.sameValue(result.nanoseconds, 0); +result = Temporal.Duration.from({ nanoseconds: -Infinity }, { disambiguation: 'constrain' }); +assert.sameValue(result.years, 0); +assert.sameValue(result.months, 0); +assert.sameValue(result.weeks, 0); +assert.sameValue(result.days, 0); +assert.sameValue(result.hours, 0); +assert.sameValue(result.minutes, 0); +assert.sameValue(result.seconds, 0); +assert.sameValue(result.milliseconds, 0); +assert.sameValue(result.microseconds, 0); +assert.sameValue(result.nanoseconds, -Number.MAX_VALUE); // balance @@ -53,26 +153,26 @@ const obj = { } }; -assert.throws(RangeError, () => Temporal.Duration.from({ years: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 1, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ months: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 2, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ weeks: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 3, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ days: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 4, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ hours: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 5, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ minutes: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 6, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ seconds: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 7, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ milliseconds: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 8, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ microseconds: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 9, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => Temporal.Duration.from({ nanoseconds: obj }, { disambiguation: 'constrain' })); -assert.sameValue(calls, 10, "it fails after fetching the primitive value"); +result = Temporal.Duration.from({ years: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 1, "it fetches the primitive value"); +result = Temporal.Duration.from({ months: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 2, "it fetches the primitive value"); +result = Temporal.Duration.from({ weeks: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 3, "it fetches the primitive value"); +result = Temporal.Duration.from({ days: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 4, "it fetches the primitive value"); +result = Temporal.Duration.from({ hours: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 5, "it fetches the primitive value"); +result = Temporal.Duration.from({ minutes: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 6, "it fetches the primitive value"); +result = Temporal.Duration.from({ seconds: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 7, "it fetches the primitive value"); +result = Temporal.Duration.from({ milliseconds: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 8, "it fetches the primitive value"); +result = Temporal.Duration.from({ microseconds: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 9, "it fetches the primitive value"); +result = Temporal.Duration.from({ nanoseconds: obj }, { disambiguation: 'constrain' }); +assert.sameValue(calls, 10, "it fetches the primitive value"); assert.throws(RangeError, () => Temporal.Duration.from({ years: obj }, { disambiguation: 'balance' })); assert.sameValue(calls, 11, "it fails after fetching the primitive value"); diff --git a/polyfill/test/Duration/constructor/from/subclass-invalid-arg.js b/polyfill/test/Duration/constructor/from/subclass-invalid-arg.js index 176b18ed02..44ea600bad 100644 --- a/polyfill/test/Duration/constructor/from/subclass-invalid-arg.js +++ b/polyfill/test/Duration/constructor/from/subclass-invalid-arg.js @@ -15,5 +15,5 @@ class MyDuration extends Temporal.Duration { } assert.throws(RangeError, () => MyDuration.from({ years: Infinity }, { disambiguation: "reject" })); -assert.throws(RangeError, () => MyDuration.from({ days: -1 }, { disambiguation: "reject" })); +assert.throws(RangeError, () => MyDuration.from({ days: -Infinity }, { disambiguation: "reject" })); assert.sameValue(called, false); diff --git a/polyfill/test/Duration/prototype/minus/infinity-throws-rangeerror.js b/polyfill/test/Duration/prototype/minus/infinity-throws-rangeerror.js index a097b2b2fe..e63b7fe871 100644 --- a/polyfill/test/Duration/prototype/minus/infinity-throws-rangeerror.js +++ b/polyfill/test/Duration/prototype/minus/infinity-throws-rangeerror.js @@ -8,18 +8,18 @@ esid: sec-temporal.duration.prototype.minus const instance = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); -// balanceConstrain +// constrain -assert.throws(RangeError, () => instance.minus({ years: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ months: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ weeks: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ days: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ hours: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ minutes: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ seconds: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ milliseconds: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ microseconds: Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ nanoseconds: Infinity }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ years: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ months: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ weeks: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ days: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ hours: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ minutes: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ seconds: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ milliseconds: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ microseconds: Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ nanoseconds: Infinity }, { disambiguation: 'constrain' })); // balance @@ -34,6 +34,19 @@ assert.throws(RangeError, () => instance.minus({ milliseconds: Infinity }, { dis assert.throws(RangeError, () => instance.minus({ microseconds: Infinity }, { disambiguation: 'balance' })); assert.throws(RangeError, () => instance.minus({ nanoseconds: Infinity }, { disambiguation: 'balance' })); +// reject + +assert.throws(RangeError, () => instance.minus({ years: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ months: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ weeks: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ days: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ hours: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ minutes: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ seconds: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ milliseconds: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ microseconds: Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ nanoseconds: Infinity }, { disambiguation: 'reject' })); + let calls = 0; const obj = { valueOf() { @@ -42,25 +55,25 @@ const obj = { } }; -assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 1, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ months: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ months: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 2, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ weeks: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ weeks: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 3, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ days: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ days: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 4, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ hours: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ hours: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 5, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ minutes: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ minutes: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 6, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ seconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ seconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 7, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ milliseconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ milliseconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 8, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 9, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 10, "it fails after fetching the primitive value"); assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'balance' })); @@ -83,3 +96,24 @@ assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambig assert.sameValue(calls, 19, "it fails after fetching the primitive value"); assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'balance' })); assert.sameValue(calls, 20, "it fails after fetching the primitive value"); + +assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 21, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ months: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 22, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ weeks: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 23, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ days: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 24, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ hours: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 25, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ minutes: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 26, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ seconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 27, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ milliseconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 28, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 29, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 30, "it fails after fetching the primitive value"); diff --git a/polyfill/test/Duration/prototype/minus/negative-infinity-throws-rangeerror.js b/polyfill/test/Duration/prototype/minus/negative-infinity-throws-rangeerror.js index a74f3c01ea..656b6d26ab 100644 --- a/polyfill/test/Duration/prototype/minus/negative-infinity-throws-rangeerror.js +++ b/polyfill/test/Duration/prototype/minus/negative-infinity-throws-rangeerror.js @@ -8,18 +8,18 @@ esid: sec-temporal.duration.prototype.minus const instance = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); -// balanceConstrain +// constrain -assert.throws(RangeError, () => instance.minus({ years: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ months: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ weeks: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ days: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ hours: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ minutes: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ seconds: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ milliseconds: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ microseconds: -Infinity }, { disambiguation: 'balanceConstrain' })); -assert.throws(RangeError, () => instance.minus({ nanoseconds: -Infinity }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ years: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ months: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ weeks: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ days: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ hours: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ minutes: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ seconds: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ milliseconds: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ microseconds: -Infinity }, { disambiguation: 'constrain' })); +assert.throws(RangeError, () => instance.minus({ nanoseconds: -Infinity }, { disambiguation: 'constrain' })); // balance @@ -34,6 +34,19 @@ assert.throws(RangeError, () => instance.minus({ milliseconds: -Infinity }, { di assert.throws(RangeError, () => instance.minus({ microseconds: -Infinity }, { disambiguation: 'balance' })); assert.throws(RangeError, () => instance.minus({ nanoseconds: -Infinity }, { disambiguation: 'balance' })); +// reject + +assert.throws(RangeError, () => instance.minus({ years: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ months: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ weeks: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ days: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ hours: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ minutes: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ seconds: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ milliseconds: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ microseconds: -Infinity }, { disambiguation: 'reject' })); +assert.throws(RangeError, () => instance.minus({ nanoseconds: -Infinity }, { disambiguation: 'reject' })); + let calls = 0; const obj = { valueOf() { @@ -42,25 +55,25 @@ const obj = { } }; -assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 1, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ months: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ months: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 2, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ weeks: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ weeks: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 3, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ days: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ days: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 4, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ hours: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ hours: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 5, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ minutes: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ minutes: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 6, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ seconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ seconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 7, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ milliseconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ milliseconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 8, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 9, "it fails after fetching the primitive value"); -assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'balanceConstrain' })); +assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'constrain' })); assert.sameValue(calls, 10, "it fails after fetching the primitive value"); assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'balance' })); @@ -83,3 +96,24 @@ assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambig assert.sameValue(calls, 19, "it fails after fetching the primitive value"); assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'balance' })); assert.sameValue(calls, 20, "it fails after fetching the primitive value"); + +assert.throws(RangeError, () => instance.minus({ years: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 21, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ months: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 22, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ weeks: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 23, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ days: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 24, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ hours: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 25, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ minutes: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 26, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ seconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 27, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ milliseconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 28, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ microseconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 29, "it fails after fetching the primitive value"); +assert.throws(RangeError, () => instance.minus({ nanoseconds: obj }, { disambiguation: 'reject' })); +assert.sameValue(calls, 30, "it fails after fetching the primitive value"); diff --git a/polyfill/test/Duration/prototype/minus/subclass-out-of-range.js b/polyfill/test/Duration/prototype/minus/subclass-out-of-range.js index 03f3d0a262..4ead32d962 100644 --- a/polyfill/test/Duration/prototype/minus/subclass-out-of-range.js +++ b/polyfill/test/Duration/prototype/minus/subclass-out-of-range.js @@ -11,14 +11,26 @@ let called = 0; class MyDuration extends Temporal.Duration { constructor(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) { ++called; - assert.compareArray([years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds], Array(10).fill(0)); + assert.compareArray([years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds], [0, 0, 0, 0, 0, 0, 0, 0, 0, -Number.MAX_VALUE]); super(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); } } -const instance = MyDuration.from("PT0S"); +const instance = MyDuration.from({ nanoseconds: -Number.MAX_VALUE }); assert.sameValue(called, 1); -assert.throws(RangeError, () => instance.minus({ nanoseconds: 1 }, { disambiguation: "balanceConstrain" })); -assert.throws(RangeError, () => instance.minus({ nanoseconds: 1 }, { disambiguation: "balance" })); -assert.sameValue(called, 1); +const result = instance.minus({ nanoseconds: Number.MAX_VALUE }); +assert.sameValue(result.years, 0, "years result"); +assert.sameValue(result.months, 0, "months result"); +assert.sameValue(result.weeks, 0, "weekss result"); +assert.sameValue(result.days, 0, "days result"); +assert.sameValue(result.hours, 0, "hours result"); +assert.sameValue(result.minutes, 0, "minutes result"); +assert.sameValue(result.seconds, 0, "seconds result"); +assert.sameValue(result.milliseconds, 0, "milliseconds result"); +assert.sameValue(result.microseconds, 0, "microseconds result"); +assert.sameValue(result.nanoseconds, -Number.MAX_VALUE, "nanoseconds result"); +assert.sameValue(called, 2); + +assert.throws(RangeError, () => instance.minus({ nanoseconds: Number.MAX_VALUE }, { disambiguation: "reject" })); +assert.sameValue(called, 2); diff --git a/polyfill/test/absolute.mjs b/polyfill/test/absolute.mjs index 2f39803767..55cf182951 100644 --- a/polyfill/test/absolute.mjs +++ b/polyfill/test/absolute.mjs @@ -373,6 +373,9 @@ describe('Absolute', () => { throws(() => abs.plus({ months: 1 }), RangeError); throws(() => abs.plus({ weeks: 1 }), RangeError); }); + it('mixed positive and negative values always throw', () => { + throws(() => abs.plus({ hours: 1, minutes: -30 }), RangeError); + }); }); describe('Absolute.minus works', () => { const abs = Absolute.from('1969-12-25T12:23:45.678901234Z'); @@ -385,6 +388,9 @@ describe('Absolute', () => { throws(() => abs.minus({ months: 1 }), RangeError); throws(() => abs.minus({ weeks: 1 }), RangeError); }); + it('mixed positive and negative values always throw', () => { + throws(() => abs.minus({ hours: 1, minutes: -30 }), RangeError); + }); }); describe('Absolute.compare works', () => { const abs1 = Absolute.from('1963-02-13T09:36:29.123456789Z'); diff --git a/polyfill/test/date.mjs b/polyfill/test/date.mjs index 08d95b8aa8..3073ef98b9 100644 --- a/polyfill/test/date.mjs +++ b/polyfill/test/date.mjs @@ -274,6 +274,12 @@ describe('Date', () => { const jan31 = Date.from('2020-01-31'); throws(() => jan31.plus({ months: 1 }, { disambiguation: 'reject' }), RangeError); }); + it('symmetrical with regard to negative durations', () => { + equal(`${Date.from('2019-11-18').plus({ years: -43 })}`, '1976-11-18'); + equal(`${Date.from('1977-02-18').plus({ months: -3 })}`, '1976-11-18'); + equal(`${Date.from('1976-12-08').plus({ days: -20 })}`, '1976-11-18'); + equal(`${Date.from('2019-02-28').plus({ months: -1 })}`, '2019-01-28'); + }); it("ignores lower units that don't balance up to a day", () => { equal(`${date.plus({ hours: 1 })}`, '1976-11-18'); equal(`${date.plus({ minutes: 1 })}`, '1976-11-18'); @@ -297,6 +303,11 @@ describe('Date', () => { throws(() => date.plus({ months: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => date.plus({ months: 1, days: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('date.minus() works', () => { const date = Date.from('2019-11-18'); @@ -324,6 +335,12 @@ describe('Date', () => { const mar31 = Date.from('2020-03-31'); throws(() => mar31.minus({ months: 1 }, { disambiguation: 'reject' }), RangeError); }); + it('symmetrical with regard to negative durations', () => { + equal(`${Date.from('1976-11-18').minus({ years: -43 })}`, '2019-11-18'); + equal(`${Date.from('2018-12-18').minus({ months: -11 })}`, '2019-11-18'); + equal(`${Date.from('2019-10-29').minus({ days: -20 })}`, '2019-11-18'); + equal(`${Date.from('2019-01-28').minus({ months: -1 })}`, '2019-02-28'); + }); it("ignores lower units that don't balance up to a day", () => { equal(`${date.minus({ hours: 1 })}`, '2019-11-18'); equal(`${date.minus({ minutes: 1 })}`, '2019-11-18'); @@ -347,6 +364,11 @@ describe('Date', () => { throws(() => date.minus({ months: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => date.minus({ months: 1, days: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('date.toString() works', () => { it('new Date(1976, 11, 18).toString()', () => { diff --git a/polyfill/test/datemath.mjs b/polyfill/test/datemath.mjs index b1f0220c25..3064e62df1 100644 --- a/polyfill/test/datemath.mjs +++ b/polyfill/test/datemath.mjs @@ -70,8 +70,11 @@ function buildSub(one, two, largestUnits) { largestUnits.forEach((largestUnit) => { describe(`< ${one} : ${two} (${largestUnit})>`, () => { const dif = two.difference(one, { largestUnit }); - it(`(${one}).plus(${dif}) => ${two}`, () => assert(one.plus(dif, { disambiguation: 'reject' }).equals(two))); - it(`(${two}).minus(${dif}) => ${one}`, () => assert(two.minus(dif, { disambiguation: 'reject' }).equals(one))); + const disambiguation = 'reject'; + it(`(${one}).plus(${dif}) => ${two}`, () => assert(one.plus(dif, { disambiguation }).equals(two))); + it(`(${two}).minus(${dif}) => ${one}`, () => assert(two.minus(dif, { disambiguation }).equals(one))); + it(`(${one}).minus(-${dif}) => ${two}`, () => assert(one.minus(dif.negated(), { disambiguation }).equals(two))); + it(`(${two}).plus(-${dif}) => ${one}`, () => assert(two.plus(dif.negated(), { disambiguation }).equals(one))); }); }); } diff --git a/polyfill/test/datetime.mjs b/polyfill/test/datetime.mjs index 44c6154858..a57b198048 100644 --- a/polyfill/test/datetime.mjs +++ b/polyfill/test/datetime.mjs @@ -322,6 +322,10 @@ describe('DateTime', () => { it('throws if out of order', () => throws(() => earlier.difference(later), RangeError)); it(`(${earlier}).plus(${diff}) == (${later})`, () => assert(earlier.plus(diff).equals(later))); it(`(${later}).minus(${diff}) == (${earlier})`, () => assert(later.minus(diff).equals(earlier))); + it('symmetrical with regard to negative durations', () => { + assert(earlier.minus(diff.negated()).equals(later)); + assert(later.plus(diff.negated()).equals(earlier)); + }); }); }); describe('date/time maths: hours overflow', () => { @@ -335,15 +339,22 @@ describe('DateTime', () => { const later = earlier.plus({ hours: 2 }); equal(`${later}`, '2020-06-01T01:12:38.271986102'); }); + it('symmetrical with regard to negative durations', () => { + equal(`${DateTime.from('2019-10-29T10:46:38.271986102').plus({ hours: -12 })}`, '2019-10-28T22:46:38.271986102'); + equal(`${DateTime.from('2020-05-31T23:12:38.271986102').minus({ hours: -2 })}`, '2020-06-01T01:12:38.271986102'); + }); }); describe('DateTime.plus() works', () => { + const jan31 = DateTime.from('2020-01-31T15:00'); it('constrain when ambiguous result', () => { - const jan31 = DateTime.from('2020-01-31T15:00'); equal(`${jan31.plus({ months: 1 })}`, '2020-02-29T15:00'); equal(`${jan31.plus({ months: 1 }, { disambiguation: 'constrain' })}`, '2020-02-29T15:00'); }); + it('symmetrical with regard to negative durations in the time part', () => { + equal(`${jan31.plus({ minutes: -30 })}`, '2020-01-31T14:30'); + equal(`${jan31.plus({ seconds: -30 })}`, '2020-01-31T14:59:30'); + }); it('throw when ambiguous result with reject', () => { - const jan31 = DateTime.from('2020-01-31T15:00:00'); throws(() => jan31.plus({ months: 1 }, { disambiguation: 'reject' }), RangeError); }); it('invalid disambiguation', () => { @@ -351,15 +362,23 @@ describe('DateTime', () => { throws(() => DateTime.from('2019-11-18T15:00').plus({ months: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => jan31.plus({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('date.minus() works', () => { + const mar31 = DateTime.from('2020-03-31T15:00'); it('constrain when ambiguous result', () => { - const mar31 = DateTime.from('2020-03-31T15:00'); equal(`${mar31.minus({ months: 1 })}`, '2020-02-29T15:00'); equal(`${mar31.minus({ months: 1 }, { disambiguation: 'constrain' })}`, '2020-02-29T15:00'); }); + it('symmetrical with regard to negative durations in the time part', () => { + equal(`${mar31.minus({ minutes: -30 })}`, '2020-03-31T15:30'); + equal(`${mar31.minus({ seconds: -30 })}`, '2020-03-31T15:00:30'); + }); it('throw when ambiguous result with reject', () => { - const mar31 = DateTime.from('2020-03-31T15:00'); throws(() => mar31.minus({ months: 1 }, { disambiguation: 'reject' }), RangeError); }); it('invalid disambiguation', () => { @@ -367,6 +386,11 @@ describe('DateTime', () => { throws(() => DateTime.from('2019-11-18T15:00').minus({ months: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => mar31.plus({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('DateTime.difference()', () => { const dt = DateTime.from('1976-11-18T15:23:30.123456789'); diff --git a/polyfill/test/duration.mjs b/polyfill/test/duration.mjs index a732a3fdf9..209eb7cf7c 100644 --- a/polyfill/test/duration.mjs +++ b/polyfill/test/duration.mjs @@ -19,6 +19,9 @@ describe('Duration', () => { equal(typeof Duration.prototype, 'object'); }); describe('Duration.prototype', () => { + it('Duration.prototype has sign', () => { + assert('sign' in Duration.prototype); + }); it('Duration.prototype.with is a Function', () => { equal(typeof Duration.prototype.with, 'function'); }); @@ -31,10 +34,69 @@ describe('Duration', () => { it('Duration.prototype.getFields is a Function', () => { equal(typeof Duration.prototype.getFields, 'function'); }); + it('Duration.prototype.negated is a Function', () => { + equal(typeof Duration.prototype.negated, 'function'); + }); + it('Duration.prototype.abs is a Function', () => { + equal(typeof Duration.prototype.abs, 'function'); + }); }); }); describe('Construction', () => { - it('negative values throw', () => throws(() => new Duration(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1), RangeError)); + it('positive duration, sets fields', () => { + const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 0); + equal(d.sign, 1); + equal(d.years, 5); + equal(d.months, 5); + equal(d.weeks, 5); + equal(d.days, 5); + equal(d.hours, 5); + equal(d.minutes, 5); + equal(d.seconds, 5); + equal(d.milliseconds, 5); + equal(d.microseconds, 5); + equal(d.nanoseconds, 0); + }); + it('negative duration, sets fields', () => { + const d = new Duration(-5, -5, -5, -5, -5, -5, -5, -5, -5, 0); + equal(d.sign, -1); + equal(d.years, -5); + equal(d.months, -5); + equal(d.weeks, -5); + equal(d.days, -5); + equal(d.hours, -5); + equal(d.minutes, -5); + equal(d.seconds, -5); + equal(d.milliseconds, -5); + equal(d.microseconds, -5); + equal(d.nanoseconds, 0); + }); + it('zero-length, sets fields', () => { + const d = new Duration(); + equal(d.sign, 0); + equal(d.years, 0); + equal(d.months, 0); + equal(d.weeks, 0); + equal(d.days, 0); + equal(d.hours, 0); + equal(d.minutes, 0); + equal(d.seconds, 0); + equal(d.milliseconds, 0); + equal(d.microseconds, 0); + equal(d.nanoseconds, 0); + }); + it('mixed positive and negative values throw', () => { + throws(() => new Duration(-1, 1, 1, 1, 1, 1, 1, 1, 1, 1), RangeError); + throws(() => new Duration(1, -1, 1, 1, 1, 1, 1, 1, 1, 1), RangeError); + throws(() => new Duration(1, 1, -1, 1, 1, 1, 1, 1, 1, 1), RangeError); + throws(() => new Duration(1, 1, 1, -1, 1, 1, 1, 1, 1, 1), RangeError); + throws(() => new Duration(1, 1, 1, 1, -1, 1, 1, 1, 1, 1), RangeError); + throws(() => new Duration(1, 1, 1, 1, 1, -1, 1, 1, 1, 1), RangeError); + throws(() => new Duration(1, 1, 1, 1, 1, 1, -1, 1, 1, 1), RangeError); + throws(() => new Duration(1, 1, 1, 1, 1, 1, 1, -1, 1, 1), RangeError); + throws(() => new Duration(1, 1, 1, 1, 1, 1, 1, 1, -1, 1), RangeError); + throws(() => new Duration(1, 1, 1, 1, 1, 1, 1, 1, 1, -1), RangeError); + }); }); describe('from()', () => { it('Duration.from(P5Y) is not the same object', () => { @@ -73,28 +135,36 @@ describe('Duration', () => { ].forEach((str) => throws(() => Duration.from(str), RangeError)); }); it('"P" by itself is not a valid string', () => { - throws(() => Duration.from('P'), RangeError); - throws(() => Duration.from('PT'), RangeError); + ['P', 'PT', '-P', '-PT', '+P', '+PT'].forEach((s) => throws(() => Duration.from(s), RangeError)); }); it('no junk at end of string', () => throws(() => Duration.from('P1Y1M1W1DT1H1M1.01Sjunk'), RangeError)); + it('with a + sign', () => { + const d = Duration.from('+P1D'); + equal(d.days, 1); + }); + it('with a - sign', () => { + const d = Duration.from('-P1D'); + equal(d.days, -1); + }); + it('all units have the same sign', () => { + const d = Duration.from('-P1Y1M1W1DT1H1M1.123456789S'); + equal(d.years, -1); + equal(d.months, -1); + equal(d.weeks, -1); + equal(d.days, -1); + equal(d.hours, -1); + equal(d.minutes, -1); + equal(d.seconds, -1); + equal(d.milliseconds, -123); + equal(d.microseconds, -456); + equal(d.nanoseconds, -789); + }); + it('does not accept minus signs in individual units', () => { + throws(() => Duration.from('P-1Y1M'), RangeError); + throws(() => Duration.from('P1Y-1M'), RangeError); + }); describe('Disambiguation', () => { - it('negative values always throw', () => { - const negative = { - years: -1, - months: -1, - days: -1, - hours: -1, - minutes: -1, - seconds: -1, - milliseconds: -1, - microseconds: -1, - nanoseconds: -1 - }; - ['constrain', 'balance', 'reject'].forEach((disambiguation) => - throws(() => Duration.from(negative, { disambiguation }), RangeError) - ); - }); - it('negative values cannot balance', () => { + it('mixed positive and negative values always throw', () => { ['constrain', 'balance', 'reject'].forEach((disambiguation) => throws(() => Duration.from({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) ); @@ -138,6 +208,9 @@ describe('Duration', () => { equal(`${new Duration(0, 0, 0, 0, 0, 0, 0, 1111, 1111, 1111)}`, 'PT1.112112111S'); equal(`${Duration.from({ seconds: 120, milliseconds: 3500 })}`, 'PT123.500S'); }); + it('emits a negative sign for a negative duration', () => { + equal(`${Duration.from({ weeks: -1, days: -1 })}`, '-P1W1D'); + }); }); describe('toLocaleString()', () => { it('produces an implementation-defined string', () => { @@ -279,16 +352,29 @@ describe('Duration', () => { equal(result.microseconds, 3); equal(result.nanoseconds, 1); }); - it('negative values always throw', () => { + it('mixed positive and negative values always throw', () => { ['constrain', 'balance', 'reject'].forEach((disambiguation) => - throws(() => duration.with({ minutes: -1 }, { disambiguation }), RangeError) + throws(() => duration.with({ hours: 1, minutes: -1 }, { disambiguation }), RangeError) ); }); + it('can reverse the sign if all the fields are replaced', () => { + const d = Duration.from({ years: 5, days: 1 }); + const d2 = d.with({ years: -1, days: -1, minutes: 0 }); + equal(`${d2}`, '-P1Y1D'); + notEqual(d.sign, d2.sign); + }); + it('throws if new fields have a different sign from the old fields', () => { + const d = Duration.from({ years: 5, days: 1 }); + throws(() => d.with({ months: -5, minutes: 0 }), RangeError); + }); it('invalid disambiguation', () => { ['', 'CONSTRAIN', 'xyz', 3, null].forEach((disambiguation) => throws(() => duration.with({ days: 5 }, { disambiguation }), RangeError) ); }); + it('sign cannot be manipulated independently', () => { + throws(() => duration.with({ sign: -1 }), RangeError); + }); }); describe('Duration.plus()', () => { const duration = Duration.from({ days: 1, minutes: 5 }); @@ -298,6 +384,13 @@ describe('Duration', () => { it('adds different units', () => { equal(`${duration.plus({ hours: 12, seconds: 30 })}`, 'P1DT12H5M30S'); }); + it('symmetric with regard to negative durations', () => { + equal(`${Duration.from('P3DT10M').plus({ days: -2, minutes: -5 })}`, 'P1DT5M'); + equal( + `${Duration.from('P1DT12H5M30S').plus({ hours: -12, seconds: -30 }, { disambiguation: 'balance' })}`, + 'P1DT5M' + ); + }); it('does not balance units', () => { const d = Duration.from('P50M50W50DT50H50M50.500500500S'); const result = d.plus(d); @@ -342,10 +435,15 @@ describe('Duration', () => { throws(() => max.plus(max, { disambiguation: 'reject' }), RangeError); }); it('throws on invalid disambiguation', () => { - ['', 'CONSTRAIN', 'balance', 3, null].forEach((disambiguation) => + ['', 'CONSTRAIN', 'balanceConstrain', 3, null].forEach((disambiguation) => throws(() => duration.plus(duration, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'balance', 'reject'].forEach((disambiguation) => + throws(() => duration.plus({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('Duration.minus()', () => { const duration = Duration.from({ days: 3, hours: 1, minutes: 10 }); @@ -363,7 +461,15 @@ describe('Duration', () => { it('balances when subtracting different units', () => { equal(`${duration.minus({ seconds: 30 })}`, 'P3DT1H9M30S'); }); - it('never balances positive units in balanceConstrain mode', () => { + it('symmetric with regard to negative durations', () => { + equal(`${Duration.from('P2DT1H5M').minus({ days: -1, minutes: -5 })}`, 'P3DT1H10M'); + equal(`${new Duration().minus({ days: -3, hours: -1, minutes: -10 })}`, 'P3DT1H10M'); + equal(`${Duration.from('PT1H10M').minus({ days: -3 })}`, 'P3DT1H10M'); + equal(`${Duration.from('P3DT1H').minus({ minutes: -10 })}`, 'P3DT1H10M'); + equal(`${Duration.from('P3DT55M').minus({ minutes: -15 }, { disambiguation: 'balance' })}`, 'P3DT1H10M'); + equal(`${Duration.from('P3DT1H9M30S').minus({ seconds: -30 }, { disambiguation: 'balance' })}`, 'P3DT1H10M'); + }); + it('never balances positive units in constrain mode', () => { const d = Duration.from({ minutes: 100, seconds: 100, @@ -385,7 +491,7 @@ describe('Duration', () => { equal(result.microseconds, 1500); equal(result.nanoseconds, 1500); - result = d.minus(less, { disambiguation: 'balanceConstrain' }); + result = d.minus(less, { disambiguation: 'constrain' }); equal(result.minutes, 90); equal(result.seconds, 90); equal(result.milliseconds, 1500); @@ -423,14 +529,14 @@ describe('Duration', () => { }); const tenYears = Duration.from('P10Y'); const tenMinutes = Duration.from('PT10M'); - it('throws if result is negative', () => { - ['balanceConstrain', 'balance'].forEach((disambiguation) => { - throws(() => tenYears.minus({ years: 15 }, { disambiguation }), RangeError); - throws(() => tenMinutes.minus({ minutes: 15 }, { disambiguation }), RangeError); - }); + it('has correct negative result', () => { + let result = tenYears.minus({ years: 15 }); + equal(result.years, -5); + result = tenMinutes.minus({ minutes: 15 }); + equal(result.minutes, -5); }); it('throws if result cannot be determined to be positive or negative', () => { - ['balanceConstrain', 'balance'].forEach((disambiguation) => { + ['constrain', 'balance'].forEach((disambiguation) => { throws(() => tenYears.minus({ months: 5 }, { disambiguation }), RangeError); throws(() => tenYears.minus({ weeks: 5 }, { disambiguation }), RangeError); throws(() => tenYears.minus({ days: 5 }, { disambiguation }), RangeError); @@ -440,10 +546,15 @@ describe('Duration', () => { }); }); it('throws on invalid disambiguation', () => { - ['', 'BALANCE', 'constrain', 3, null].forEach((disambiguation) => + ['', 'BALANCE', 'xyz', 3, null].forEach((disambiguation) => throws(() => duration.minus(duration, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'balance', 'reject'].forEach((disambiguation) => + throws(() => duration.minus({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('duration.getFields() works', () => { const d1 = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5); @@ -499,6 +610,20 @@ describe('Duration', () => { equal(d2.microseconds, 5); equal(d2.nanoseconds, 5); }); + it('has correct sign', () => { + const fields = Duration.from('-P5Y5M5W5DT5H5M5.005005005S'); + equal(fields.years, -5); + equal(fields.months, -5); + equal(fields.weeks, -5); + equal(fields.days, -5); + equal(fields.hours, -5); + equal(fields.minutes, -5); + equal(fields.seconds, -5); + equal(fields.milliseconds, -5); + equal(fields.microseconds, -5); + equal(fields.nanoseconds, -5); + }); + it('does not include the sign field', () => assert(!('sign' in fields))); }); describe("Comparison operators don't work", () => { const d1 = Duration.from('P3DT1H'); @@ -511,6 +636,49 @@ describe('Duration', () => { it('<=', () => throws(() => d1 <= d2)); it('>=', () => throws(() => d1 >= d2)); }); + describe('Duration.negated()', () => { + it('makes a positive duration negative', () => { + const pos = Duration.from('P3DT1H'); + const neg = pos.negated(); + equal(`${neg}`, '-P3DT1H'); + equal(neg.sign, -1); + }); + it('makes a negative duration positive', () => { + const neg = Duration.from('-PT2H20M30S'); + const pos = neg.negated(); + equal(`${pos}`, 'PT2H20M30S'); + equal(pos.sign, 1); + }); + it('makes a copy of a zero duration', () => { + const zero = Duration.from('PT0S'); + const zero2 = zero.negated(); + equal(`${zero}`, `${zero2}`); + notEqual(zero, zero2); + equal(zero2.sign, 0); + }); + }); + describe('Duration.abs()', () => { + it('makes a copy of a positive duration', () => { + const pos = Duration.from('P3DT1H'); + const pos2 = pos.abs(); + equal(`${pos}`, `${pos2}`); + notEqual(pos, pos2); + equal(pos2.sign, 1); + }); + it('makes a negative duration positive', () => { + const neg = Duration.from('-PT2H20M30S'); + const pos = neg.abs(); + equal(`${pos}`, 'PT2H20M30S'); + equal(pos.sign, 1); + }); + it('makes a copy of a zero duration', () => { + const zero = Duration.from('PT0S'); + const zero2 = zero.abs(); + equal(`${zero}`, `${zero2}`); + notEqual(zero, zero2); + equal(zero2.sign, 0); + }); + }); }); import { normalize } from 'path'; diff --git a/polyfill/test/time.mjs b/polyfill/test/time.mjs index 47699918ea..20a5bd1bbd 100644 --- a/polyfill/test/time.mjs +++ b/polyfill/test/time.mjs @@ -304,6 +304,11 @@ describe('Time', () => { it(`(${time}).plus({ nanoseconds: 300 })`, () => { equal(`${time.plus({ nanoseconds: 300 })}`, '15:23:30.123457089'); }); + it('symmetric with regard to negative durations', () => { + equal(`${Time.from('07:23:30.123456789').plus({ hours: -16 })}`, '15:23:30.123456789'); + equal(`${Time.from('16:08:30.123456789').plus({ minutes: -45 })}`, '15:23:30.123456789'); + equal(`${Time.from('15:23:30.123457089').plus({ nanoseconds: -300 })}`, '15:23:30.123456789'); + }); it('time.plus(durationObj)', () => { equal(`${time.plus(Temporal.Duration.from('PT16H'))}`, '07:23:30.123456789'); }); @@ -317,6 +322,11 @@ describe('Time', () => { throws(() => time.plus({ hours: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => time.plus({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('time.minus() works', () => { const time = Time.from('15:23:30.123456789'); @@ -329,6 +339,14 @@ describe('Time', () => { equal(`${time.minus({ microseconds: 800 })}`, '15:23:30.122656789')); it(`(${time}).minus({ nanoseconds: 800 })`, () => equal(`${time.minus({ nanoseconds: 800 })}`, '15:23:30.123455989')); + it('symmetric with regard to negative durations', () => { + equal(`${Time.from('23:23:30.123456789').minus({ hours: -16 })}`, '15:23:30.123456789'); + equal(`${Time.from('14:38:30.123456789').minus({ minutes: -45 })}`, '15:23:30.123456789'); + equal(`${Time.from('15:22:45.123456789').minus({ seconds: -45 })}`, '15:23:30.123456789'); + equal(`${Time.from('15:23:29.323456789').minus({ milliseconds: -800 })}`, '15:23:30.123456789'); + equal(`${Time.from('15:23:30.122656789').minus({ microseconds: -800 })}`, '15:23:30.123456789'); + equal(`${Time.from('15:23:30.123455989').minus({ nanoseconds: -800 })}`, '15:23:30.123456789'); + }); it('time.minus(durationObj)', () => { equal(`${time.minus(Temporal.Duration.from('PT16H'))}`, '23:23:30.123456789'); }); @@ -342,6 +360,11 @@ describe('Time', () => { throws(() => time.minus({ hours: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => time.minus({ hours: 1, minutes: -30 }, { disambiguation }), RangeError) + ); + }); }); describe('time.toString() works', () => { it('new Time(15, 23).toString()', () => { diff --git a/polyfill/test/validStrings.mjs b/polyfill/test/validStrings.mjs index 91e1fea8c6..8499f3a59d 100644 --- a/polyfill/test/validStrings.mjs +++ b/polyfill/test/validStrings.mjs @@ -231,52 +231,54 @@ const date = choice(seq(dateYear, '-', dateMonth, '-', dateDay), seq(dateYear, d const time = seq(timeSpec, [timeZone]); const dateTime = choice(date, seq(date, dateTimeSeparator, time)); -// A repeat of timeFractionalPart and timeFraction because of the different -// property names between Duration and the other types const durationFractionalPart = withCode(between(1, 9, digit()), (data, result) => { const fraction = result.padEnd(9, '0'); - data.milliseconds = +fraction.slice(0, 3); - data.microseconds = +fraction.slice(3, 6); - data.nanoseconds = +fraction.slice(6, 9); + data.milliseconds = +fraction.slice(0, 3) * data.factor; + data.microseconds = +fraction.slice(3, 6) * data.factor; + data.nanoseconds = +fraction.slice(6, 9) * data.factor; }); const durationFraction = seq(decimalSeparator, durationFractionalPart); const durationSeconds = seq( - withCode(oneOrMore(digit()), (data, result) => (data.seconds = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.seconds = +result * data.factor)), [durationFraction], secondsDesignator ); const durationMinutes = seq( - withCode(oneOrMore(digit()), (data, result) => (data.minutes = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.minutes = +result * data.factor)), minutesDesignator, [durationSeconds] ); const durationHours = seq( - withCode(oneOrMore(digit()), (data, result) => (data.hours = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.hours = +result * data.factor)), hoursDesignator, [durationMinutes] ); const durationTime = seq(durationTimeDesignator, choice(durationHours, durationMinutes, durationSeconds)); const durationDays = seq( - withCode(oneOrMore(digit()), (data, result) => (data.days = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.days = +result * data.factor)), daysDesignator ); const durationWeeks = seq( - withCode(oneOrMore(digit()), (data, result) => (data.weeks = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.weeks = +result * data.factor)), weeksDesignator, [durationDays] ); const durationMonths = seq( - withCode(oneOrMore(digit()), (data, result) => (data.months = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.months = +result * data.factor)), monthsDesignator, [durationWeeks] ); const durationYears = seq( - withCode(oneOrMore(digit()), (data, result) => (data.years = +result)), + withCode(oneOrMore(digit()), (data, result) => (data.years = +result * data.factor)), yearsDesignator, [durationMonths] ); const durationDate = seq(choice(durationYears, durationMonths, durationWeeks, durationDays), [durationTime]); -const duration = seq(durationDesignator, choice(durationDate, durationTime)); +const duration = seq( + withCode([sign], (data, result) => (data.factor = result === '-' ? -1 : 1)), + durationDesignator, + choice(durationDate, durationTime) +); const absolute = seq(date, dateTimeSeparator, timeSpec, timeZone); diff --git a/polyfill/test/yearmonth.mjs b/polyfill/test/yearmonth.mjs index 075862e948..1613667b71 100644 --- a/polyfill/test/yearmonth.mjs +++ b/polyfill/test/yearmonth.mjs @@ -219,6 +219,10 @@ describe('YearMonth', () => { equal(`${ym.plus({ years: 1 }, { disambiguation: 'constrain' })}`, '2020-11'); equal(`${ym.plus({ years: 1 }, { disambiguation: 'reject' })}`, '2020-11'); }); + it('symmetrical with regard to negative durations', () => { + equal(`${YearMonth.from('2020-01').plus({ months: -2 })}`, '2019-11'); + equal(`${YearMonth.from('2020-11').plus({ years: -1 })}`, '2019-11'); + }); it('yearMonth.plus(durationObj)', () => { equal(`${ym.plus(Temporal.Duration.from('P2M'))}`, '2020-01'); }); @@ -259,6 +263,11 @@ describe('YearMonth', () => { throws(() => ym.plus({ months: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => ym.plus({ years: 1, months: -6 }, { disambiguation }), RangeError) + ); + }); }); describe('YearMonth.minus() works', () => { const ym = YearMonth.from('2019-11'); @@ -272,6 +281,10 @@ describe('YearMonth', () => { equal(`${ym.minus({ years: 12 }, { disambiguation: 'constrain' })}`, '2007-11'); equal(`${ym.minus({ years: 12 }, { disambiguation: 'reject' })}`, '2007-11'); }); + it('symmetrical with regard to negative durations', () => { + equal(`${YearMonth.from('2018-12').minus({ months: -11 })}`, '2019-11'); + equal(`${YearMonth.from('2007-11').minus({ years: -12 })}`, '2019-11'); + }); it('yearMonth.minus(durationObj)', () => { equal(`${ym.minus(Temporal.Duration.from('P11M'))}`, '2018-12'); }); @@ -311,6 +324,11 @@ describe('YearMonth', () => { throws(() => ym.minus({ months: 1 }, { disambiguation }), RangeError) ); }); + it('mixed positive and negative values always throw', () => { + ['constrain', 'reject'].forEach((disambiguation) => + throws(() => ym.minus({ years: 1, months: -6 }, { disambiguation }), RangeError) + ); + }); }); describe('Min/max range', () => { it('constructing from numbers', () => { diff --git a/spec/absolute.html b/spec/absolute.html index 5f35dd3da1..c43f7cbcae 100644 --- a/spec/absolute.html +++ b/spec/absolute.html @@ -239,6 +239,7 @@

Temporal.Absolute.prototype.plus ( _temporalDurationLike_ )

1. Let _absolute_ be the *this* value. 1. Perform ? RequireInternalSlot(_absolute_, [[InitializedTemporalAbsolute]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « *"years"*, *"months"* »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _ns_ be _absolute_.[[Nanoseconds]] + _duration_.[[Nanoseconds]] + _duration_.[[Microseconds]] × 1000 + @@ -262,6 +263,7 @@

Temporal.Absolute.prototype.minus ( _temporalDurationLike_ )

1. Let _absolute_ be the *this* value. 1. Perform ? RequireInternalSlot(_absolute_, [[InitializedTemporalAbsolute]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « *"years"*, *"months"* »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _ns_ be _absolute_.[[Nanoseconds]] - _duration_.[[Nanoseconds]] - _duration_.[[Microseconds]] × 1000 - diff --git a/spec/abstractops.html b/spec/abstractops.html index 1f511a18a2..9d5fabd37f 100644 --- a/spec/abstractops.html +++ b/spec/abstractops.html @@ -47,13 +47,6 @@

ToTimeZoneTemporalDisambiguation ( _options_ )

- -

ToDurationSubtractionTemporalDisambiguation ( _options_ )

- - 1. Return ? GetOption(_options_, *"disambiguation"*, « *"balanceConstrain"*, *"balance"* », *"balanceConstrain"*). - -
-

ToLargestTemporalUnit ( _largestUnit_, _disallowedUnits_, _defaultUnit_ )

@@ -402,8 +395,8 @@

ISO 8601 grammar

DurationDays DurationTime? Duration : - DurationDesignator DurationDate - DurationDesignator DurationTime + Sign? DurationDesignator DurationDate + Sign? DurationDesignator DurationTime TemporalAbsoluteString : Date DateTimeSeparator TimeSpec TimeZone @@ -567,7 +560,11 @@

ParseTemporalDurationString ( _isoString_ )

1. Assert: Type(_isoString_) is String. 1. If _isoString_ does not satisfy the syntax of a |TemporalDurationString| (see ), then 1. Throw a *RangeError* exception. - 1. Let _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, and _fraction_ be the parts of _isoString_ produced respectively by the |DurationYears|, |DurationMonths|, |DurationWeeks|, |DurationDays|, |DurationHours|, |DurationMinutes|, |DurationSeconds|, and |TimeFractionalPart| productions, or *undefined* if not present. + 1. Let _sign_, _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, and _fraction_ be the parts of _isoString_ produced respectively by the |Sign|, |DurationYears|, |DurationMonths|, |DurationWeeks|, |DurationDays|, |DurationHours|, |DurationMinutes|, |DurationSeconds|, and |TimeFractionalPart| productions, or *undefined* if not present. + 1. If _sign_ is *"-"*, then + 1. Let _factor_ be −1;. + 1. Else, + 1. Let _factor_ be 1. 1. Set _years_ to ? ToInteger(_years_). 1. Set _months_ to ? ToInteger(_months_). 1. Set _weeks_ to ? ToInteger(_weeks_). @@ -589,16 +586,16 @@

ParseTemporalDurationString ( _isoString_ )

1. Let _microseconds_ be 0. 1. Let _nanoseconds_ be 0. 1. Return the new Record { - [[Years]]: _years_, - [[Months]]: _months_, - [[Weeks]]: _weeks_, - [[Days]]: _days_, - [[Hours]]: _hours_, - [[Minutes]]: _minutes_, - [[Seconds]]: _seconds_, - [[Milliseconds]]: _milliseconds_, - [[Microseconds]]: _microseconds_, - [[Nanoseconds]]: _nanoseconds_ + [[Years]]: _years_ × _factor_, + [[Months]]: _months_ × _factor_, + [[Weeks]]: _weeks_ × _factor_, + [[Days]]: _days_ × _factor_, + [[Hours]]: _hours_ × _factor_, + [[Minutes]]: _minutes_ × _factor_, + [[Seconds]]: _seconds_ × _factor_, + [[Milliseconds]]: _milliseconds_ × _factor_, + [[Microseconds]]: _microseconds_ × _factor_, + [[Nanoseconds]]: _nanoseconds_ × _factor_ }.
diff --git a/spec/date.html b/spec/date.html index e50054490d..07aeec7033 100644 --- a/spec/date.html +++ b/spec/date.html @@ -301,6 +301,7 @@

Temporal.Date.prototype.plus ( _temporalDurationLike_ [ , _options_ ] )

1. Let _temporalDate_ be the *this* value. 1. Perform ? RequireInternalSlot(_temporalDate_, [[InitializedTemporalDate]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _balanceResult_ be ? BalanceDuration(_duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], *"days"*). 1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_). 1. Let _result_ be ? AddDate(_temporalDate_.[[ISOYear]], _temporalDate_.[[ISOMonth]], _temporalDate_.[[ISODay]], _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], _disambiguation_). @@ -319,6 +320,7 @@

Temporal.Date.prototype.minus ( _temporalDurationLike_ [ , _options_ ] )

Temporal.DateTime.prototype.plus ( _temporalDurationLike_ [ , _options_ ] )< 1. Let _dateTime_ be the *this* value. 1. Perform ? RequireInternalSlot(_dateTime_, [[InitializedTemporalDateTime]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_). 1. Let _timePart_ be ? AddTime(_dateTime_.[[Hour]], _dateTime_.[[Minute]], _dateTime_.[[Second]], _dateTime_.[[Millisecond]], _dateTime_.[[Microsecond]], _dateTime_.[[Nanosecond]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _datePart_ be ? AddDate(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _disambiguation_). @@ -406,6 +407,7 @@

Temporal.DateTime.prototype.minus ( _temporalDurationLike_ [ , _options_ ] ) 1. Let _dateTime_ be the *this* value. 1. Perform ? RequireInternalSlot(_dateTime_, [[InitializedTemporalDateTime]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_). 1. Let _timePart_ be ? SubtractTime(_dateTime_.[[Hour]], _dateTime_.[[Minute]], _dateTime_.[[Second]], _dateTime_.[[Millisecond]], _dateTime_.[[Microsecond]], _dateTime_.[[Nanosecond]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _datePart_ be ? SubtractDate(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]] - _timePart_.[[Day]], _disambiguation_). diff --git a/spec/duration.html b/spec/duration.html index c716ac0032..b5d96ae6b9 100644 --- a/spec/duration.html +++ b/spec/duration.html @@ -254,6 +254,19 @@

get Temporal.Duration.prototype.nanoseconds

+ +

get Temporal.Duration.prototype.sign

+

+ `Temporal.Duration.prototype.sign` is an accessor property whose set accessor function is undefined. + Its get accessor function performs the following steps: +

+ + 1. Let _duration_ be the *this* value. + 1. Perform ? RequireInternalSlot(_duration_, [[InitializedTemporalDuration]]). + 1. Return ! DurationSign(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). + +
+

Temporal.Duration.prototype.with ( _temporalDurationLike_ [ , _options_ ] )

@@ -310,6 +323,32 @@

Temporal.Duration.prototype.with ( _temporalDurationLike_ [ , _options_ ] )< + +

Temporal.Duration.prototype.negated ( )

+

+ The `negated` method takes no arguments. + It performs the following steps when called: +

+ + 1. Let _duration_ be the *this* value. + 1. Perform ? RequireInternalSlot(_duration_, [[InitializedTemporalDuration]]). + 1. Return ? CreateTemporalDurationFromInstance(_duration_, −_duration_.[[Years]], −_duration_.[[Months]], −_duration_.[[Weeks]], −_duration_.[[Days]], −_duration_.[[Hours]], −_duration_.[[Minutes]], −_duration_.[[Seconds]], −_duration_.[[Milliseconds]], −_duration_.[[Microseconds]], −_duration_.[[Nanoseconds]]). + +
+ + +

Temporal.Duration.prototype.abs ( )

+

+ The `abs` method takes no arguments. + It performs the following steps when called: +

+ + 1. Let _duration_ be the *this* value. + 1. Perform ? RequireInternalSlot(_duration_, [[InitializedTemporalDuration]]). + 1. Return ? CreateTemporalDurationFromInstance(_duration_, abs(_duration_.[[Years]]), abs(_duration_.[[Months]]), abs(_duration_.[[Weeks]]), abs(_duration_.[[Days]]), abs(_duration_.[[Hours]]), abs(_duration_.[[Minutes]]), abs(_duration_.[[Seconds]]), abs(_duration_.[[Milliseconds]]), abs(_duration_.[[Microseconds]]), abs(_duration_.[[Nanoseconds]])). + +
+

Temporal.Duration.prototype.plus ( _other_ [ , _options_ ] )

@@ -320,8 +359,9 @@

Temporal.Duration.prototype.plus ( _other_ [ , _options_ ] )

1. Let _duration_ be the *this* value. 1. Perform ? RequireInternalSlot(_duration_, [[InitializedTemporalDuration]]). 1. Set _other_ to ? ToLimitedTemporalDuration(_other_, « »). + 1. Perform ? RejectDurationSign (_other_.[[Years]], _other_.[[Months]], _other_.[[Weeks]], _other_.[[Days]], _other_.[[Hours]], _other_.[[Minutes]], _other_.[[Seconds]], _other_.[[Milliseconds]], _other_.[[Microseconds]], _other_.[[Nanoseconds]]). 1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_). - 1. Let _result_ be ? AddDuration(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _other_.[[Years]], _other_.[[Months]], _other_.[[Weeks]], _other_.[[Days]], _other_.[[Hours]], _other_.[[Minutes]], _other_.[[Seconds]], _other_.[[Milliseconds]], _other_.[[Microseconds]], _other_.[[Nanoseconds]], _disambiguation_). + 1. Let _result_ be ? DurationArithmetic(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _other_.[[Years]], _other_.[[Months]], _other_.[[Weeks]], _other_.[[Days]], _other_.[[Hours]], _other_.[[Minutes]], _other_.[[Seconds]], _other_.[[Milliseconds]], _other_.[[Microseconds]], _other_.[[Nanoseconds]], _disambiguation_). 1. Return ? CreateTemporalDurationFromInstance(_duration_, _result_.[[Years]], _result_.[[Months]], _result_.[[Weeks]], _result_.[[Days]], _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
@@ -336,8 +376,9 @@

Temporal.Duration.prototype.minus ( _other_ [ , _options_ ] )

1. Let _duration_ be the *this* value. 1. Perform ? RequireInternalSlot(_duration_, [[InitializedTemporalDuration]]). 1. Set _other_ to ? ToLimitedTemporalDuration(_other_, « »). - 1. Let _disambiguation_ be ? ToDurationSubtractionTemporalDisambiguation(_options_). - 1. Let _result_ be ? SubtractDuration(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _other_.[[Years]], _other_.[[Months]], _other_.[[Weeks]], _other_.[[Days]], _other_.[[Hours]], _other_.[[Minutes]], _other_.[[Seconds]], _other_.[[Milliseconds]], _other_.[[Microseconds]], _other_.[[Nanoseconds]], _disambiguation_). + 1. Perform ? RejectDurationSign (_other_.[[Years]], _other_.[[Months]], _other_.[[Weeks]], _other_.[[Days]], _other_.[[Hours]], _other_.[[Minutes]], _other_.[[Seconds]], _other_.[[Milliseconds]], _other_.[[Microseconds]], _other_.[[Nanoseconds]]). + 1. Let _disambiguation_ be ? ToDurationTemporalDisambiguation(_options_). + 1. Let _result_ be ? DurationArithmetic(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], −_other_.[[Years]], −_other_.[[Months]], −_other_.[[Weeks]], −_other_.[[Days]], −_other_.[[Hours]], −_other_.[[Minutes]], −_other_.[[Seconds]], −_other_.[[Milliseconds]], −_other_.[[Microseconds]], −_other_.[[Nanoseconds]], _disambiguation_). 1. Return ? CreateTemporalDurationFromInstance(_duration_, _result_.[[Years]], _result_.[[Months]], _result_.[[Weeks]], _result_.[[Days]], _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
@@ -438,10 +479,35 @@

ToTemporalDurationRecord ( _temporalDurationLike_ )

+ +

DurationSign ( _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_ )

+ + 1. For each value _v_ of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, do + 1. If _v_ < 0, return −1. + 1. If _v_ > 0, return 1. + 1. Return 0. + +
+ + +

RejectDurationSign ( _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_ )

+ + 1. Let _sign_ be ! DurationSign(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_). + 1. For each value _v_ of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, do + 1. If _v_ < 0 and _sign_ > 0, throw a *RangeError* exception. + 1. If _v_ > 0 and _sign_ < 0, throw a *RangeError* exception. + 1. Return *true*. + +
+

ValidateTemporalDuration ( _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_ )

- 1. For each of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, if any of the values is negative or infinite, return *false*. + 1. Let _sign_ be ! DurationSign(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_). + 1. For each value _v_ of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, do + 1. If _v_ is infinite, return *false*. + 1. If _v_ < 0 and _sign_ > 0, return *false*. + 1. If _v_ > 0 and _sign_ < 0, return *false*. 1. Return *true*.
@@ -568,12 +634,12 @@

BalanceDuration ( _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _mi

RegulateDuration ( _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _disambiguation_ )

1. Assert: _disambiguation_ is one of *"constrain"*, *"balance"*, or *"reject"*. + 1. Perform ? RejectDurationSign(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_). 1. If _disambiguation_ is *"reject"*, then 1. If ! ValidateTemporalDuration(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_) is *false*, then 1. Throw a *RangeError* exception. 1. Else if _disambiguation_ is *"constrain"*, then - 1. For each of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, if any of the values is negative, throw a *RangeError* exception. - 1. For each of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, if any of the values is infinite, let it be the largest possible finite value of the Number type. + 1. For each of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, if any of the values is infinite, let it be the sign of that value times the largest possible finite value of the Number type. 1. Else if _disambiguation_ is *"balance"*, then 1. If ! ValidateTemporalDuration(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_) is *false*, then 1. Throw a *RangeError* exception. @@ -602,11 +668,11 @@

RegulateDuration ( _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _ - -

AddDuration ( _y1_, _mon1_, _w1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus1_, _ns1_, _y2_, _mon2_, _w2_, _d2_, _h2_, _min2_, _s2_, _ms2_, _mus2_, _ns2_, _disambiguation_ )

+ +

DurationArithmetic ( _y1_, _mon1_, _w1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus1_, _ns1_, _y2_, _mon2_, _w2_, _d2_, _h2_, _min2_, _s2_, _ms2_, _mus2_, _ns2_, _disambiguation_ )

1. Assert: _y1_, _mon1_, _w1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus1_, _ns1_, _y2_, _mon2_, _w2_, _d2_, _h2_, _min2_, _s2_, _ms2_, _mus2_, _ns2_ are integer Number values. - 1. Assert: _disambiguation_ is either *"reject"* or *"constrain"*. + 1. Assert: _disambiguation_ is either *"reject"*, *"balance"*, or *"constrain"*. 1. Let _nanoseconds_ be _ns1_ + _ns2_. 1. Let _microseconds_ be _mus1_ + _mus2_. 1. Let _milliseconds_ be _ms1_ + _ms2_. @@ -617,40 +683,17 @@

AddDuration ( _y1_, _mon1_, _w1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus1_, _ 1. Let _weeks_ be _w1_ + _w2_. 1. Let _months_ be _mon1_ + _mon2_. 1. Let _years_ be _y1_ + _y2_. - 1. If _disambiguation_ is *"constrain"*, then - 1. For each of _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, if any of the values is infinite, let it be the largest possible finite value of the Number type. - 1. If ! ValidateTemporalDuration(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_) is *false*, then - 1. Throw a *RangeError* exception. - 1. Return the new Record { - [[Years]]: _years_, - [[Months]]: _months_, - [[Weeks]]: _weeks_, - [[Days]]: _days_, - [[Hours]]: _hours_, - [[Minutes]]: _minutes_, - [[Seconds]]: _seconds_, - [[Milliseconds]]: _milliseconds_, - [[Microseconds]]: _microseconds_, - [[Nanoseconds]]: _nanoseconds_ - }. - - - - -

SubtractDuration ( _y1_, _mon1_, _w1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus1_, _ns1_, _y2_, _mon2_, _w2_, _d2_, _h2_, _min2_, _s2_, _ms2_, _mus2_, _ns2_, _disambiguation_ )

- - 1. Assert: _y1_, _mon1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus1_, _ns1_, _y2_, _mon2_, _d2_, _h2_, _min2_, _s2_, _ms2_, _mus2_, _ns2_ are integer Number values. - 1. Assert: _disambiguation_ is either *"balanceConstrain"* or *"balance"*. - 1. Let _nanoseconds_ be _ns1_ − _ns2_. - 1. Let _microseconds_ be _mus1_ − _mus2_. - 1. Let _milliseconds_ be _ms1_ − _ms2_. - 1. Let _seconds_ be _s1_ − _s2_. - 1. Let _minutes_ be _min1_ − _min2_. - 1. Let _hours_ be _h1_ − _h2_. - 1. Let _days_ be _d1_ − _d2_. - 1. Let _weeks_ be _w1_ − _w2_. - 1. Let _months_ be _mon1_ − _mon2_. - 1. Let _years_ be _y1_ − _y2_. + 1. Let _sign_ be ! DurationSign(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_). + 1. Set _years_ to _years_ × _sign_. + 1. Set _months_ to _months_ × _sign_. + 1. Set _weeks_ to _weeks_ × _sign_. + 1. Set _days_ to _days_ × _sign_. + 1. Set _hours_ to _hours_ × _sign_. + 1. Set _minutes_ to _minutes_ × _sign_. + 1. Set _seconds_ to _seconds_ × _sign_. + 1. Set _milliseconds_ to _milliseconds_ × _sign_. + 1. Set _microseconds_ to _microseconds_ × _sign_. + 1. Set _nanoseconds_ to _nanoseconds_ × _sign_. 1. If _nanoseconds_ < 0, then 1. Set _microseconds_ to _microseconds_ + floor(_nanoseconds_ / 1000). 1. Set _nanoseconds_ to ! NonNegativeModulo(_nanoseconds_, 1000). @@ -669,22 +712,19 @@

SubtractDuration ( _y1_, _mon1_, _w1_, _d1_, _h1_, _min1_, _s1_, _ms1_, _mus 1. If _hours_ < 0, then 1. Set _days_ to _days_ + floor(_hours_ / 24). 1. Set _hours_ to ! NonNegativeModulo(_hours_, 24). - 1. If _disambiguation_ is *"balance"*, then - 1. Return ? RegulateDuration(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _disambiguation_). - 1. If ! ValidateTemporalDuration(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_) is *false*, then - 1. Throw a *RangeError* exception. - 1. Return the new Record { - [[Years]]: _years_, - [[Months]]: _months_, - [[Weeks]]: _weeks_, - [[Days]]: _days_, - [[Hours]]: _hours_, - [[Minutes]]: _minutes_, - [[Seconds]]: _seconds_, - [[Milliseconds]]: _milliseconds_, - [[Microseconds]]: _microseconds_, - [[Nanoseconds]]: _nanoseconds_ - }. + 1. If any of _months_, _weeks_, _days_ is negative, then + 1. Throw a *RangeError*. + 1. Set _years_ to _years_ × _sign_. + 1. Set _months_ to _months_ × _sign_. + 1. Set _weeks_ to _weeks_ × _sign_. + 1. Set _days_ to _days_ × _sign_. + 1. Set _hours_ to _hours_ × _sign_. + 1. Set _minutes_ to _minutes_ × _sign_. + 1. Set _seconds_ to _seconds_ × _sign_. + 1. Set _milliseconds_ to _milliseconds_ × _sign_. + 1. Set _microseconds_ to _microseconds_ × _sign_. + 1. Set _nanoseconds_ to _nanoseconds_ × _sign_. + 1. Return ? RegulateDuration(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _disambiguation_). @@ -714,33 +754,38 @@

TemporalDurationToString ( _duration_ )

1. Let _hours_ be _duration_.[[Hours]]. 1. Let _minutes_ be _duration_.[[Minutes]]. 1. Let _seconds_ be _duration_.[[Seconds]]. - 1. Let _balanceResult_ be ? BalanceSubSecond(_duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). + 1. Let _milliseconds_ be _duration_.[[Milliseconds]]. + 1. Let _microseconds_ be _duration_.[[Microseconds]]. + 1. Let _nanoseconds_ be _duration_.[[Nanoseconds]]. + 1. Let _sign_ be ! DurationSign(_years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_). + 1. Let _balanceResult_ be ? BalanceSubSecond(_milliseconds_, _microseconds_, _nanoseconds_). 1. Set _seconds_ to _seconds_ + _balanceResult_.[[Seconds]]. - 1. Let _milliseconds_ be _balanceResult_.[[Millisecond]]. - 1. Let _microseconds_ be _balanceResult_.[[Microsecond]]. - 1. Let _nanoseconds_ be _balanceResult_.[[Nanosecond]]. + 1. Set _milliseconds_ to _balanceResult_.[[Millisecond]]. + 1. Set _microseconds_ to _balanceResult_.[[Microsecond]]. + 1. Set _nanoseconds_ to _balanceResult_.[[Nanosecond]]. 1. If _years_, _months_, _weeks_, _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, and _nanoseconds_ are all 0, then 1. Return the string `"PT0S"`. 1. Let _datePart_ be `""`. 1. If _years_ is not 0, then - 1. Set _datePart_ to the string concatenation of _years_ formatted as a decimal number and the code unit 0x0059 (LATIN CAPITAL LETTER Y). + 1. Set _datePart_ to the string concatenation of abs(_years_) formatted as a decimal number and the code unit 0x0059 (LATIN CAPITAL LETTER Y). 1. If _months_ is not 0, then - 1. Set _datePart_ to the string concatenation of _datePart_, _months_ formatted as a decimal number, and the code unit 0x004D (LATIN CAPITAL LETTER M). + 1. Set _datePart_ to the string concatenation of _datePart_, abs(_months_) formatted as a decimal number, and the code unit 0x004D (LATIN CAPITAL LETTER M). 1. If _weeks_ is not 0, then - 1. Set _datePart_ to the string concatenation of _datePart_, _weeks_ formatted as a decimal number, and the code unit 0x0057 (LATIN CAPITAL LETTER W). + 1. Set _datePart_ to the string concatenation of _datePart_, abs(_weeks_) formatted as a decimal number, and the code unit 0x0057 (LATIN CAPITAL LETTER W). 1. If _days_ is not 0, then - 1. Set _datePart_ to the string concatenation of _datePart_, _days_ formatted as a decimal number, and the code unit 0x0044 (LATIN CAPITAL LETTER D). + 1. Set _datePart_ to the string concatenation of _datePart_, abs(_days_) formatted as a decimal number, and the code unit 0x0044 (LATIN CAPITAL LETTER D). 1. Let _timePart_ be `""`. 1. If _hours_ is not 0, then - 1. Set _timePart_ to the string concatenation of _hours_ formatted as a decimal number and the code unit 0x0048 (LATIN CAPITAL LETTER H). + 1. Set _timePart_ to the string concatenation of abs(_hours_) formatted as a decimal number and the code unit 0x0048 (LATIN CAPITAL LETTER H). 1. If _minutes_ is not 0, then - 1. Set _timePart_ to the string concatenation of _timePart_, _minutes_ formatted as a decimal number, and the code unit 0x004D (LATIN CAPITAL LETTER M). - 1. Let _secondsPart_ be ! FormatSecondsStringPart(_seconds_, _milliseconds_, _microseconds_, _nanoseconds_). + 1. Set _timePart_ to the string concatenation of _timePart_, abs(_minutes_) formatted as a decimal number, and the code unit 0x004D (LATIN CAPITAL LETTER M). + 1. Let _secondsPart_ be ! FormatSecondsStringPart(abs(_seconds_), abs(_milliseconds_), abs(_microseconds_), abs(_nanoseconds_)). 1. If the first code unit of _secondsPart_ is 0x003A (COLON), then 1. Set _secondsPart_ to the string containing all the code units of _secondsPart_ except the first. 1. If _secondsPart_ is not `""`, then 1. Set _secondsPart_ to the string concatenation of _secondsPart_ and the code unit 0x0053 (LATIN CAPITAL LETTER S). - 1. Let _result_ be the string concatenation of the code unit 0x0050 (LATIN CAPITAL LETTER P) and _datePart_. + 1. Let _signPart_ be the code unit 0x002D (HYPHEN-MINUS) if _sign_ < 0, and otherwise the empty string. + 1. Let _result_ be the string concatenation of _signPart_, the code unit 0x0050 (LATIN CAPITAL LETTER P) and _datePart_. 1. If _timePart_ is not `""`, then 1. Set _result_ to the string concatenation of _result_, the code unit 0x0054 (LATIN CAPITAL LETTER T), _timePart_, and _secondsPart_. 1. Return _result_. diff --git a/spec/time.html b/spec/time.html index 79f79617de..99a7b39921 100644 --- a/spec/time.html +++ b/spec/time.html @@ -210,8 +210,13 @@

Temporal.Time.prototype.plus ( _temporalDurationLike_ [ , _options_ ] )

1. Let _temporalTime_ be the *this* value. 1. Perform ? RequireInternalSlot(_temporalTime_, [[InitializedTemporalTime]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Perform ? ToTemporalDisambiguation(_options_). - 1. Let _result_ be ? AddTime(_temporalTime_.[[Hour]], _temporalTime_.[[Minute]], _temporalTime_.[[Second]], _temporalTime_.[[Millisecond]], _temporalTime_.[[Microsecond]], _temporalTime_.[[Nanosecond]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). + 1. Let _sign_ be ! DurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). + 1. If _sign_ < 0, then + 1. Let _result_ be ? SubtractTime(_temporalTime_.[[Hour]], _temporalTime_.[[Minute]], _temporalTime_.[[Second]], _temporalTime_.[[Millisecond]], _temporalTime_.[[Microsecond]], _temporalTime_.[[Nanosecond]], −_duration_.[[Hours]], −_duration_.[[Minutes]], −_duration_.[[Seconds]], −_duration_.[[Milliseconds]], −_duration_.[[Microseconds]], −_duration_.[[Nanoseconds]]). + 1. Else, + 1. Let _result_ be ? AddTime(_temporalTime_.[[Hour]], _temporalTime_.[[Minute]], _temporalTime_.[[Second]], _temporalTime_.[[Millisecond]], _temporalTime_.[[Microsecond]], _temporalTime_.[[Nanosecond]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Return ? CreateTemporalTimeFromInstance(_temporalTime_, _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]]).
@@ -226,8 +231,13 @@

Temporal.Time.prototype.minus ( _temporalDurationLike_ [ , _options_ ] )

diff --git a/spec/yearmonth.html b/spec/yearmonth.html index 1ea5d279cd..bfa4b62ade 100644 --- a/spec/yearmonth.html +++ b/spec/yearmonth.html @@ -218,9 +218,15 @@

Temporal.YearMonth.prototype.plus ( _temporalDurationLike_ [ , _options_ ] ) 1. Let _yearMonth_ be the *this* value. 1. Perform ? RequireInternalSlot(_yearMonth_, [[InitializedTemporalYearMonth]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _balanceResult_ be ? BalanceDuration(_duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], *"days"*). 1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_). - 1. Let _result_ be ? AddDate(_yearMonth_.[[ISOYear]], _yearMonth_.[[ISOMonth]], 1, _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], _disambiguation_). + 1. Let _sign_ be ! DurationSign(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult.[[Days]], 0, 0, 0, 0, 0, 0). + 1. If _sign_ < 0, then + 1. Let _day_ be ! DaysInMonth(_yearMonth.[[ISOYear]], _yearMonth_.[[ISOMonth]]). + 1. Else, + 1. Let _day_ be 1. + 1. Let _result_ be ? AddDate(_yearMonth_.[[ISOYear]], _yearMonth_.[[ISOMonth]], _day_, _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], _disambiguation_). 1. Let _result_ be ? RegulateYearMonth(_result_.[[Year]], _result_.[[Month]], _disambiguation_). 1. Return ? CreateTemporalYearMonthFromInstance(_yearMonth_, _result_.[[Year]], _result_.[[Month]]). @@ -236,10 +242,15 @@

Temporal.YearMonth.prototype.minus ( _temporalDurationLike_ [ , _options_ ] 1. Let _yearMonth_ be the *this* value. 1. Perform ? RequireInternalSlot(_yearMonth_, [[InitializedTemporalYearMonth]]). 1. Let _duration_ be ? ToLimitedTemporalDuration(_temporalDurationLike_, « »). + 1. Perform ? RejectDurationSign(_duration_.[[Years]], _duration.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]). 1. Let _balanceResult_ be ? BalanceDuration(_duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], *"days"*). 1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_). - 1. Let _lastDay_ be ! DaysInMonth(_yearMonth_.[[ISOYear]], _yearMonth_.[[ISOMonth]]). - 1. Let _result_ be ? SubtractDate(_yearMonth_.[[ISOYear]], _yearMonth_.[[ISOMonth]], _lastDay_, _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], _disambiguation_). + 1. Let _sign_ be ! DurationSign(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult.[[Days]], 0, 0, 0, 0, 0, 0). + 1. If _sign_ < 0, then + 1. Let _day_ be 1. + 1. Else, + 1. Let _day_ be ! DaysInMonth(_yearMonth.[[ISOYear]], _yearMonth_.[[ISOMonth]]). + 1. Let _result_ be ? SubtractDate(_yearMonth_.[[ISOYear]], _yearMonth_.[[ISOMonth]], _day_, _duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _balanceResult_.[[Days]], _disambiguation_). 1. Let _result_ be ? RegulateYearMonth(_result_.[[Year]], _result_.[[Month]], _disambiguation_). 1. Return ? CreateTemporalYearMonthFromInstance(_yearMonth_, _result_.[[Year]], _result_.[[Month]]).