/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef builtin_temporal_TemporalRoundingMode_h #define builtin_temporal_TemporalRoundingMode_h #include "mozilla/Assertions.h" #include #include namespace js::temporal { // Overview of integer rounding modes is available at // . enum class TemporalRoundingMode { // 1. Directed rounding to an integer. // Round toward positive infinity. Ceil, // Round toward negative infinity. Floor, // Round toward infinity or round away from zero. Expand, // Round toward zero or round away from infinity. Trunc, // 2. Rounding to the nearest integer. // Round half toward positive infinity. HalfCeil, // Round half toward negative infinity. HalfFloor, // Round half toward infinity or round half away from zero. HalfExpand, // Round half toward zero or round half away from infinity. HalfTrunc, // Round half to even. HalfEven, }; /** * NegateTemporalRoundingMode ( roundingMode ) */ constexpr auto NegateTemporalRoundingMode(TemporalRoundingMode roundingMode) { // Steps 1-5. switch (roundingMode) { case TemporalRoundingMode::Ceil: return TemporalRoundingMode::Floor; case TemporalRoundingMode::Floor: return TemporalRoundingMode::Ceil; case TemporalRoundingMode::HalfCeil: return TemporalRoundingMode::HalfFloor; case TemporalRoundingMode::HalfFloor: return TemporalRoundingMode::HalfCeil; case TemporalRoundingMode::Expand: case TemporalRoundingMode::Trunc: case TemporalRoundingMode::HalfExpand: case TemporalRoundingMode::HalfTrunc: case TemporalRoundingMode::HalfEven: return roundingMode; } MOZ_CRASH("invalid rounding mode"); } /** * Adjust the rounding mode to round negative values in the same direction as * positive values. */ constexpr auto ToPositiveRoundingMode(TemporalRoundingMode roundingMode) { switch (roundingMode) { case TemporalRoundingMode::Ceil: case TemporalRoundingMode::Floor: case TemporalRoundingMode::HalfCeil: case TemporalRoundingMode::HalfFloor: case TemporalRoundingMode::HalfEven: // (Half-)Ceil/Floor round toward the same infinity for negative and // positive values, so the rounding mode doesn't need to be adjusted. The // same applies for half-even rounding. return roundingMode; case TemporalRoundingMode::Expand: // Expand rounds positive values toward +infinity, but negative values // toward -infinity. Adjust the rounding mode to Ceil to round negative // values in the same direction as positive values. return TemporalRoundingMode::Ceil; case TemporalRoundingMode::Trunc: // Truncation rounds positive values down toward zero, but negative values // up toward zero. Adjust the rounding mode to Floor to round negative // values in the same direction as positive values. return TemporalRoundingMode::Floor; case TemporalRoundingMode::HalfExpand: // Adjust the rounding mode to Half-Ceil, similar to the Expand case. return TemporalRoundingMode::HalfCeil; case TemporalRoundingMode::HalfTrunc: // Adjust the rounding mode to Half-Floor, similar to the Trunc case. return TemporalRoundingMode::HalfFloor; } MOZ_CRASH("unexpected rounding mode"); } // Temporal performs division on "mathematical values" [1] with implies using // infinite precision. This rules out using IEE-754 floating point types like // `double`. It also means we can't implement the algorithms from the // specification verbatim, but instead have to translate them into equivalent // operations. // // Throughout the following division functions, the divisor is required to be // positive. This allows to simplify the implementation, because it ensures // non-zero quotient and remainder values have the same sign as the dividend. // // [1] https://tc39.es/ecma262/#mathematical-value /** * Compute ceiling division ⌈dividend / divisor⌉. The divisor must be a positive * number. */ constexpr int64_t CeilDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // Ceiling division rounds the quotient toward positive infinity. When the // quotient is negative, this is equivalent to rounding toward zero. See [1]. // // int64_t division truncates, so rounding toward zero for negative quotients // is already covered. When there is a non-zero positive remainder, the // quotient is positive and we have to increment it by one to implement // rounding toward positive infinity. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (remainder > 0) { quotient += 1; } return quotient; } /** * Compute floor division ⌊dividend / divisor⌋. The divisor must be a positive * number. */ constexpr int64_t FloorDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // Floor division rounds the quotient toward negative infinity. When the // quotient is positive, this is equivalent to rounding toward zero. See [1]. // // int64_t division truncates, so rounding toward zero for positive quotients // is already covered. When there is a non-zero negative remainder, the // quotient is negative and we have to decrement it by one to implement // rounding toward negative infinity. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (remainder < 0) { quotient -= 1; } return quotient; } /** * Compute "round toward infinity" division `dividend / divisor`. The divisor * must be a positive number. */ constexpr int64_t ExpandDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // "Round toward infinity" division rounds positive quotients toward positive // infinity and negative quotients toward negative infinity. See [1]. // // When there is a non-zero positive remainder, the quotient is positive and // we have to increment it by one to implement rounding toward positive // infinity. When there is a non-zero negative remainder, the quotient is // negative and we have to decrement it by one to implement rounding toward // negative infinity. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (remainder > 0) { quotient += 1; } if (remainder < 0) { quotient -= 1; } return quotient; } /** * Compute truncating division `dividend / divisor`. The divisor must be a * positive number. */ constexpr int64_t TruncDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // Truncating division rounds both positive and negative quotients toward // zero, cf. [1]. // // int64_t division truncates, so rounding toward zero implicitly happens. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes return dividend / divisor; } /** * Compute "round half toward positive infinity" division `dividend / divisor`. * The divisor must be a positive number. */ inline int64_t HalfCeilDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // "Round half toward positive infinity" division rounds the quotient toward // positive infinity when the fractional part of the remainder is ≥0.5. When // the quotient is negative, this is equivalent to rounding toward zero // instead of toward positive infinity. See [1]. // // When the remainder is a non-zero positive value, the quotient is positive, // too. When additionally the fractional part of the remainder is ≥0.5, we // have to increment the quotient by one to implement rounding toward positive // infinity. // // int64_t division truncates, so we implicitly round toward zero for negative // quotients. When the absolute value of the fractional part of the remainder // is >0.5, we should instead have rounded toward negative infinity, so we // need to decrement the quotient by one. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (remainder > 0 && uint64_t(std::abs(remainder)) * 2 >= uint64_t(divisor)) { quotient += 1; } if (remainder < 0 && uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { quotient -= 1; } return quotient; } /** * Compute "round half toward negative infinity" division `dividend / divisor`. * The divisor must be a positive number. */ inline int64_t HalfFloorDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // "Round half toward negative infinity" division rounds the quotient toward // negative infinity when the fractional part of the remainder is ≥0.5. When // the quotient is positive, this is equivalent to rounding toward zero // instead of toward negative infinity. See [1]. // // When the remainder is a non-zero negative value, the quotient is negative, // too. When additionally the fractional part of the remainder is ≥0.5, we // have to decrement the quotient by one to implement rounding toward negative // infinity. // // int64_t division truncates, so we implicitly round toward zero for positive // quotients. When the absolute value of the fractional part of the remainder // is >0.5, we should instead have rounded toward positive infinity, so we // need to increment the quotient by one. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (remainder < 0 && uint64_t(std::abs(remainder)) * 2 >= uint64_t(divisor)) { quotient -= 1; } if (remainder > 0 && uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { quotient += 1; } return quotient; } /** * Compute "round half toward infinity" division `dividend / divisor`. The * divisor must be a positive number. */ inline int64_t HalfExpandDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // "Round half toward infinity" division rounds positive quotients whose // remainder has a fractional part ≥0.5 toward positive infinity. And negative // quotients whose remainder has a fractional part ≥0.5 toward negative // infinity. See [1]. // // int64_t division truncates, which means it rounds toward zero, so we have // to increment resp. decrement the quotient when the fractional part of the // remainder is ≥0.5 to round toward ±infinity. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (uint64_t(std::abs(remainder)) * 2 >= uint64_t(divisor)) { quotient += (dividend > 0) ? 1 : -1; } return quotient; } /** * Compute "round half toward zero" division `dividend / divisor`. The divisor * must be a positive number. */ inline int64_t HalfTruncDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // "Round half toward zero" division rounds both positive and negative // quotients whose remainder has a fractional part ≤0.5 toward zero. See [1]. // // int64_t division truncates, so we implicitly round toward zero. When the // fractional part of the remainder is >0.5, we should instead have rounded // toward ±infinity, so we need to increment resp. decrement the quotient by // one. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if (uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { quotient += (dividend > 0) ? 1 : -1; } return quotient; } /** * Compute "round half to even" division `dividend / divisor`. The divisor must * be a positive number. */ inline int64_t HalfEvenDiv(int64_t dividend, int64_t divisor) { MOZ_ASSERT(divisor > 0, "negative divisor not supported"); // NB: Division and modulo operation are fused into a single machine code // instruction by the compiler. int64_t quotient = dividend / divisor; int64_t remainder = dividend % divisor; // "Round half to even" division rounds both positive and negative quotients // to the nearest even integer. See [1]. // // int64_t division truncates, so we implicitly round toward zero. When the // fractional part of the remainder is 0.5 and the quotient is odd or when the // fractional part of the remainder is >0.5, we should instead have rounded // toward ±infinity, so we need to increment resp. decrement the quotient by // one. // // [1] // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes if ((quotient & 1) == 1 && uint64_t(std::abs(remainder)) * 2 == uint64_t(divisor)) { quotient += (dividend > 0) ? 1 : -1; } if (uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { quotient += (dividend > 0) ? 1 : -1; } return quotient; } /** * Perform `dividend / divisor` and round the result according to the given * rounding mode. */ inline int64_t Divide(int64_t dividend, int64_t divisor, TemporalRoundingMode roundingMode) { switch (roundingMode) { case TemporalRoundingMode::Ceil: return CeilDiv(dividend, divisor); case TemporalRoundingMode::Floor: return FloorDiv(dividend, divisor); case TemporalRoundingMode::Expand: return ExpandDiv(dividend, divisor); case TemporalRoundingMode::Trunc: return TruncDiv(dividend, divisor); case TemporalRoundingMode::HalfCeil: return HalfCeilDiv(dividend, divisor); case TemporalRoundingMode::HalfFloor: return HalfFloorDiv(dividend, divisor); case TemporalRoundingMode::HalfExpand: return HalfExpandDiv(dividend, divisor); case TemporalRoundingMode::HalfTrunc: return HalfTruncDiv(dividend, divisor); case TemporalRoundingMode::HalfEven: return HalfEvenDiv(dividend, divisor); } MOZ_CRASH("invalid rounding mode"); } } /* namespace js::temporal */ #endif /* builtin_temporal_TemporalRoundingMode_h */