| /* |
| * Copyright (C) 2021 Igalia S.L. |
| * Copyright (C) 2021 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "TemporalInstant.h" |
| |
| #include "AuxiliaryBarrierInlines.h" |
| #include "Error.h" |
| #include "ISO8601.h" |
| #include "IntlObjectInlines.h" |
| #include "JSBigInt.h" |
| #include "JSGlobalObject.h" |
| #include "JSObjectInlines.h" |
| #include "MathCommon.h" |
| #include "StructureInlines.h" |
| #include "TemporalDuration.h" |
| #include "TemporalObject.h" |
| #include "TemporalTimeZone.h" |
| |
| namespace JSC { |
| |
| const ClassInfo TemporalInstant::s_info = { "Object"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(TemporalInstant) }; |
| |
| Structure* TemporalInstant::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) |
| { |
| return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); |
| } |
| |
| TemporalInstant::TemporalInstant(VM& vm, Structure* structure, ISO8601::ExactTime exactTime) |
| : Base(vm, structure) |
| , m_exactTime(exactTime) |
| { |
| } |
| |
| TemporalInstant* TemporalInstant::create(VM& vm, Structure* structure, ISO8601::ExactTime exactTime) |
| { |
| ASSERT(exactTime.isValid()); |
| auto* object = new (NotNull, allocateCell<TemporalInstant>(vm)) TemporalInstant(vm, structure, exactTime); |
| object->finishCreation(vm); |
| return object; |
| } |
| |
| // CreateTemporalInstant ( epochNanoseconds [ , newTarget ] ) |
| // https://tc39.es/proposal-temporal/#sec-temporal-createtemporalinstant |
| TemporalInstant* TemporalInstant::tryCreateIfValid(JSGlobalObject* globalObject, ISO8601::ExactTime exactTime, Structure* structure) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| if (!exactTime.isValid()) { |
| throwRangeError(globalObject, scope, makeString(exactTime.asString(), " epoch nanoseconds is outside of supported range for Temporal.Instant"_s)); |
| return nullptr; |
| } |
| |
| return create(vm, structure ? structure : globalObject->instantStructure(), exactTime); |
| } |
| |
| TemporalInstant* TemporalInstant::tryCreateIfValid(JSGlobalObject* globalObject, JSValue value, Structure* structure) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| JSValue epochNanoseconds = value.toBigInt(globalObject); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| #if USE(BIGINT32) |
| if (epochNanoseconds.isBigInt32()) { |
| int32_t total = epochNanoseconds.bigInt32AsInt32(); |
| ISO8601::ExactTime exactTime { total }; |
| ASSERT(exactTime.isValid()); |
| return create(vm, structure ? structure : globalObject->instantStructure(), exactTime); |
| } |
| #endif |
| |
| JSBigInt* bigint = asHeapBigInt(epochNanoseconds); |
| bool bigIntTooLong = false; |
| Int128 total; |
| if constexpr (sizeof(JSBigInt::Digit) == 4) { |
| Int128 d0 { bigint->length() > 0 ? bigint->digit(0) : 0 }; |
| Int128 d1 { bigint->length() > 1 ? bigint->digit(1) : 0 }; |
| Int128 d2 { bigint->length() > 2 ? bigint->digit(2) : 0 }; |
| total = d2 << 64 | d1 << 32 | d0; |
| bigIntTooLong = bigint->length() > 3; |
| } else { |
| ASSERT(sizeof(JSBigInt::Digit) == 8); |
| // Handle maxint128 < abs(bigint) <= maxuint128 explicitly, otherwise |
| // the int128 arithmetic below is undefined |
| if (bigint->length() > 1 && (bigint->digit(1) & 0x8000'0000'0000'0000)) { |
| total = 0; |
| bigIntTooLong = true; |
| } else { |
| Int128 low { bigint->length() > 0 ? bigint->digit(0) : 0 }; |
| Int128 high { bigint->length() > 1 ? bigint->digit(1) : 0 }; |
| total = high << 64 | low; |
| bigIntTooLong = bigint->length() > 2; |
| } |
| } |
| ISO8601::ExactTime exactTime { total * (bigint->sign() ? -1 : 1) }; |
| |
| if (bigIntTooLong || !exactTime.isValid()) { |
| String argAsString = bigint->toString(globalObject, 10); |
| if (scope.exception()) { |
| scope.clearException(); |
| argAsString = "The given number of"_s; |
| } |
| |
| throwRangeError(globalObject, scope, makeString(ellipsizeAt(100, argAsString), " epoch nanoseconds is outside of the supported range for Temporal.Instant"_s)); |
| return nullptr; |
| } |
| |
| return create(vm, structure ? structure : globalObject->instantStructure(), exactTime); |
| } |
| |
| // ToTemporalInstant ( item ) |
| // https://tc39.es/proposal-temporal/#sec-temporal-totemporalinstant |
| TemporalInstant* TemporalInstant::toInstant(JSGlobalObject* globalObject, JSValue itemValue) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| if (itemValue.inherits<TemporalInstant>()) |
| return jsCast<TemporalInstant*>(itemValue); |
| |
| // FIXME: when Temporal.ZonedDateTime lands |
| // if (itemValue.inherits<TemporalZonedDateTime>()) |
| // return TemporalInstant::create(vm, globalObject->instantStructure(), jsCast<TemporalZonedDateTime*>(itemValue)->epochTime()); |
| |
| String string = itemValue.toWTFString(globalObject); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| auto parsedExactTime = ISO8601::parseInstant(string); |
| if (!parsedExactTime) { |
| throwRangeError(globalObject, scope, makeString("'"_s, ellipsizeAt(100, string), "' is not a valid Temporal.Instant string"_s)); |
| return nullptr; |
| } |
| |
| RELEASE_AND_RETURN(scope, tryCreateIfValid(globalObject, parsedExactTime.value())); |
| } |
| |
| // Temporal.Instant.from ( item ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.from |
| TemporalInstant* TemporalInstant::from(JSGlobalObject* globalObject, JSValue itemValue) |
| { |
| VM& vm = globalObject->vm(); |
| |
| if (itemValue.inherits<TemporalInstant>()) { |
| ISO8601::ExactTime exactTime = jsCast<TemporalInstant*>(itemValue)->exactTime(); |
| return TemporalInstant::create(vm, globalObject->instantStructure(), exactTime); |
| } |
| |
| return toInstant(globalObject, itemValue); |
| } |
| |
| // Temporal.Instant.fromEpochSeconds ( epochSeconds ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochseconds |
| TemporalInstant* TemporalInstant::fromEpochSeconds(JSGlobalObject* globalObject, JSValue epochSecondsValue) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| double epochSeconds = epochSecondsValue.toNumber(globalObject); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| // NumberToBigInt step 1 |
| if (!isInteger(epochSeconds)) { |
| throwRangeError(globalObject, scope, makeString(epochSeconds, " is not a valid integer number of epoch seconds"_s)); |
| return nullptr; |
| } |
| |
| ISO8601::ExactTime exactTime = ISO8601::ExactTime::fromEpochSeconds(epochSeconds); |
| RELEASE_AND_RETURN(scope, tryCreateIfValid(globalObject, exactTime)); |
| } |
| |
| // Temporal.Instant.fromEpochMilliseconds ( epochMilliseconds ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochmilliseconds |
| TemporalInstant* TemporalInstant::fromEpochMilliseconds(JSGlobalObject* globalObject, JSValue epochMillisecondsValue) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| double epochMilliseconds = epochMillisecondsValue.toNumber(globalObject); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| // NumberToBigInt step 1 |
| if (!isInteger(epochMilliseconds)) { |
| throwRangeError(globalObject, scope, makeString(epochMilliseconds, " is not a valid integer number of epoch milliseconds"_s)); |
| return nullptr; |
| } |
| |
| ISO8601::ExactTime exactTime = ISO8601::ExactTime::fromEpochMilliseconds(epochMilliseconds); |
| RELEASE_AND_RETURN(scope, tryCreateIfValid(globalObject, exactTime)); |
| } |
| |
| // Temporal.Instant.fromEpochMicroseconds ( epochMicroseconds ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochmicroseconds |
| TemporalInstant* TemporalInstant::fromEpochMicroseconds(JSGlobalObject* globalObject, JSValue epochMicrosecondsValue) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| JSValue epochMicroseconds = epochMicrosecondsValue.toBigInt(globalObject); |
| RETURN_IF_EXCEPTION(scope, nullptr); |
| |
| #if USE(BIGINT32) |
| if (epochMicroseconds.isBigInt32()) { |
| int32_t microseconds = epochMicroseconds.bigInt32AsInt32(); |
| auto exactTime = ISO8601::ExactTime::fromEpochMicroseconds(microseconds); |
| ASSERT(exactTime.isValid()); |
| return create(vm, globalObject->instantStructure(), exactTime); |
| } |
| #endif |
| |
| JSBigInt* bigint = asHeapBigInt(epochMicroseconds); |
| bool bigIntTooLong; |
| if constexpr (sizeof(JSBigInt::Digit) == 4) { |
| bigIntTooLong = bigint->length() > 2; |
| // Handle maxint64 < abs(bigint) <= maxuint64 explicitly, otherwise the |
| // cast to int64 below is undefined |
| if (bigint->length() == 2 && (bigint->digit(1) & 0x8000'0000)) |
| bigIntTooLong = true; |
| } else { |
| ASSERT(sizeof(JSBigInt::Digit) == 8); |
| bigIntTooLong = bigint->length() > 1; |
| // Handle maxint64 < abs(bigint) <= maxuint64 explicitly, otherwise the |
| // cast to int64 below is undefined |
| if (bigint->length() == 1 && (bigint->digit(0) & 0x8000'0000'0000'0000)) |
| bigIntTooLong = true; |
| } |
| int64_t microseconds = bigIntTooLong ? 0 : JSBigInt::toBigInt64(bigint); |
| auto exactTime = ISO8601::ExactTime::fromEpochMicroseconds(microseconds); |
| |
| if (bigIntTooLong || !exactTime.isValid()) { |
| String argAsString = bigint->toString(globalObject, 10); |
| if (scope.exception()) { |
| scope.clearException(); |
| argAsString = "The given number of"_s; |
| } |
| |
| throwRangeError(globalObject, scope, makeString(ellipsizeAt(100, argAsString), " epoch microseconds is outside of supported range for Temporal.Instant"_s)); |
| return nullptr; |
| } |
| |
| return create(vm, globalObject->instantStructure(), exactTime); |
| } |
| |
| // Temporal.Instant.fromEpochNanoseconds ( epochNanoseconds ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochnanoseconds |
| TemporalInstant* TemporalInstant::fromEpochNanoseconds(JSGlobalObject* globalObject, JSValue epochNanosecondsValue) |
| { |
| return tryCreateIfValid(globalObject, epochNanosecondsValue); |
| } |
| |
| // Temporal.Instant.compare ( one, two ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.compare |
| JSValue TemporalInstant::compare(JSGlobalObject* globalObject, JSValue oneValue, JSValue twoValue) |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| TemporalInstant* one = toInstant(globalObject, oneValue); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| TemporalInstant* two = toInstant(globalObject, twoValue); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| if (one->exactTime() > two->exactTime()) |
| return jsNumber(1); |
| if (one->exactTime() < two->exactTime()) |
| return jsNumber(-1); |
| return jsNumber(0); |
| } |
| |
| ISO8601::Duration TemporalInstant::difference(JSGlobalObject* globalObject, TemporalInstant* other, JSValue optionsValue) const |
| { |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.since |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.until |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| JSObject* options = intlGetOptionsObject(globalObject, optionsValue); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| auto smallest = temporalSmallestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day }); |
| RETURN_IF_EXCEPTION(scope, { }); |
| TemporalUnit smallestUnit = smallest.value_or(TemporalUnit::Nanosecond); |
| |
| TemporalUnit defaultLargestUnit = std::min(smallestUnit, TemporalUnit::Second); |
| auto largest = temporalLargestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day }, defaultLargestUnit); |
| RETURN_IF_EXCEPTION(scope, { }); |
| TemporalUnit largestUnit = largest.value_or(defaultLargestUnit); |
| |
| if (smallest && largest && smallest < largest) { |
| throwRangeError(globalObject, scope, "smallestUnit must be smaller than largestUnit"_s); |
| return { }; |
| } |
| |
| RoundingMode roundingMode = temporalRoundingMode(globalObject, options, RoundingMode::Trunc); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| std::optional<double> maxIncrement = maximumRoundingIncrement(smallestUnit); |
| ASSERT(maxIncrement && *maxIncrement <= 1000); // unbounded increments are impossible with Temporal.Instant |
| unsigned increment = temporalRoundingIncrement(globalObject, options, maxIncrement, false); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| Int128 roundedDiff = exactTime().difference(other->exactTime(), increment, smallestUnit, roundingMode); |
| // NOTE: Duration fields are currently doubles, and the total number of |
| // nanoseconds may not fit in a double. This may need to change if the |
| // internal representation of Duration changes. |
| ASSERT(roundedDiff / 1'000'000'000 < INT64_MAX); |
| double seconds { static_cast<double>(static_cast<int64_t>(roundedDiff / 1'000'000'000)) }; |
| double nanosecondsRemainder { static_cast<double>(static_cast<int64_t>(roundedDiff % 1'000'000'000)) }; |
| ISO8601::Duration result { 0, 0, 0, 0, 0, 0, seconds, 0, 0, nanosecondsRemainder }; |
| |
| TemporalDuration::balance(result, largestUnit); |
| return result; |
| } |
| |
| static double maximumIncrement(TemporalUnit smallestUnit) |
| { |
| switch (smallestUnit) { |
| case TemporalUnit::Hour: return 24; |
| case TemporalUnit::Minute: return 1440; |
| case TemporalUnit::Second: return 86400; |
| case TemporalUnit::Millisecond: return 8.64e7; |
| case TemporalUnit::Microsecond: return 8.64e10; |
| case TemporalUnit::Nanosecond: return 8.64e13; |
| case TemporalUnit::Year: |
| case TemporalUnit::Month: |
| case TemporalUnit::Week: |
| case TemporalUnit::Day: |
| default: |
| { } |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| ISO8601::ExactTime TemporalInstant::round(JSGlobalObject* globalObject, JSValue optionsValue) const |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| JSObject* options = nullptr; |
| std::optional<TemporalUnit> smallest; |
| if (optionsValue.isString()) { |
| auto string = optionsValue.toWTFString(globalObject); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| smallest = temporalUnitType(string); |
| if (!smallest) { |
| throwRangeError(globalObject, scope, "smallestUnit is an invalid Temporal unit"_s); |
| return { }; |
| } |
| |
| if (smallest.value() <= TemporalUnit::Day) { |
| throwRangeError(globalObject, scope, "smallestUnit is a disallowed unit"_s); |
| return { }; |
| } |
| } else { |
| options = intlGetOptionsObject(globalObject, optionsValue); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| smallest = temporalSmallestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day }); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| if (!smallest) { |
| throwRangeError(globalObject, scope, "Cannot round without a smallestUnit option"_s); |
| return { }; |
| } |
| } |
| TemporalUnit smallestUnit = smallest.value(); |
| |
| RoundingMode roundingMode = temporalRoundingMode(globalObject, options, RoundingMode::HalfExpand); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| double increment = temporalRoundingIncrement(globalObject, options, maximumIncrement(smallestUnit), true); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| return exactTime().round(increment, smallestUnit, roundingMode); |
| } |
| |
| // Temporal.Instant.prototype.toString( [ options ] ) |
| // https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tostring |
| String TemporalInstant::toString(JSGlobalObject* globalObject, JSValue optionsValue) const |
| { |
| VM& vm = globalObject->vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| JSObject* options = intlGetOptionsObject(globalObject, optionsValue); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| if (!options) |
| return toString(); |
| |
| JSObject* timeZone = nullptr; |
| JSValue timeZoneValue = options->get(globalObject, vm.propertyNames->timeZone); |
| RETURN_IF_EXCEPTION(scope, { }); |
| if (!timeZoneValue.isUndefined()) { |
| timeZone = TemporalTimeZone::from(globalObject, timeZoneValue); |
| RETURN_IF_EXCEPTION(scope, { }); |
| } |
| |
| PrecisionData data = secondsStringPrecision(globalObject, options); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| RoundingMode roundingMode = temporalRoundingMode(globalObject, options, RoundingMode::Trunc); |
| RETURN_IF_EXCEPTION(scope, { }); |
| |
| // No need to make a new object if we were given explicit defaults. |
| if (std::get<0>(data.precision) == Precision::Auto && roundingMode == RoundingMode::Trunc) |
| return toString(timeZone); |
| |
| ISO8601::ExactTime newExactTime { exactTime().round(data.increment, data.unit, roundingMode) }; |
| |
| // FIXME: Missing, relies on TimeZone: |
| // 1. Let roundedInstant be ! CreateTemporalInstant(roundedNs). |
| // ... |
| // 1. If _outputTimeZone_ is *undefined*, then |
| // 1. Set _outputTimeZone_ to ! CreateTemporalTimeZone(*"UTC"*). |
| // 1. Let _isoCalendar_ be ! GetISO8601Calendar(). |
| // 1. Let _dateTime_ be ? BuiltinTimeZoneGetPlainDateTimeFor(_outputTimeZone_, _instant_, _isoCalendar_). |
| JSObject* outputTimeZone = timeZone; |
| if (outputTimeZone) { |
| throwVMError(globalObject, scope, "FIXME: Temporal.Instant.toString({timeZone}) not implemented yet"_s); |
| return { }; |
| } |
| |
| return toString(newExactTime, timeZone, data); |
| } |
| |
| // TemporalInstantToString ( instant, timeZone, precision ) |
| // https://tc39.es/proposal-temporal/#sec-temporal-temporalinstanttostring |
| String TemporalInstant::toString(ISO8601::ExactTime exactTime, JSObject* timeZone, PrecisionData precision) |
| { |
| GregorianDateTime gregorianDateTime { static_cast<double>(exactTime.epochMilliseconds()), LocalTimeOffset { } }; |
| StringBuilder builder; |
| |
| // If the year is outside the bounds of 0 and 9999 inclusive we want to |
| // use the extended year format (PadISOYear). |
| unsigned yearLength = 4; |
| if (gregorianDateTime.year() > 9999 || gregorianDateTime.year() < 0) { |
| builder.append(gregorianDateTime.year() < 0 ? '-' : '+'); |
| yearLength = 6; |
| } |
| |
| builder.append(makeString(pad('0', yearLength, std::abs(gregorianDateTime.year())), |
| '-', pad('0', 2, gregorianDateTime.month() + 1), |
| '-', pad('0', 2, gregorianDateTime.monthDay()), |
| 'T', pad('0', 2, gregorianDateTime.hour()), |
| ':', pad('0', 2, gregorianDateTime.minute()))); |
| |
| static constexpr int nsPerSecond { 1'000'000'000 }; |
| int fraction { exactTime.nanosecondsFraction() }; |
| if (fraction < 0) |
| fraction += nsPerSecond; |
| |
| formatSecondsStringPart(builder, gregorianDateTime.second(), fraction, precision); |
| |
| if (timeZone) { |
| // FIXME: Missing, relies on TimeZone: |
| // 1. Let _timeZoneString_ be ? BuiltinTimeZoneGetOffsetStringFor(_timeZone_, _instant_). |
| builder.append('Z'); |
| } else |
| builder.append('Z'); |
| |
| return builder.toString(); |
| } |
| |
| } // namespace JSC |