/**
|
* @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
|
* @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
|
* @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
|
*/
|
|
import { requireNonNull } from './assert';
|
import { DateTimeException } from './errors';
|
import { MathUtil } from './MathUtil';
|
|
import { LocalTime } from './LocalTime';
|
import { ZoneId } from './ZoneId';
|
|
import { ChronoField } from './temporal/ChronoField';
|
import { TemporalQueries } from './temporal/TemporalQueries';
|
|
import { ZoneRules } from './zone/ZoneRules';
|
|
const SECONDS_CACHE = {};
|
const ID_CACHE = {};
|
|
/**
|
*
|
* ### Static properties of Class {@link LocalDate}
|
*
|
* ZoneOffset.MAX_SECONDS = 18 * LocalTime.SECONDS_PER_HOUR;
|
*
|
* ZoneOffset.UTC = ZoneOffset.ofTotalSeconds(0);
|
*
|
* ZoneOffset.MIN = ZoneOffset.ofTotalSeconds(-ZoneOffset.MAX_SECONDS);
|
*
|
* ZoneOffset.MAX = ZoneOffset.ofTotalSeconds(ZoneOffset.MAX_SECONDS);
|
*
|
*/
|
export class ZoneOffset extends ZoneId {
|
/**
|
*
|
* @param {number} totalSeconds
|
* @private
|
*/
|
constructor(totalSeconds){
|
super();
|
ZoneOffset._validateTotalSeconds(totalSeconds);
|
this._totalSeconds = MathUtil.safeToInt(totalSeconds);
|
this._rules = ZoneRules.of(this);
|
this._id = ZoneOffset._buildId(totalSeconds);
|
}
|
|
/**
|
*
|
* @returns {number}
|
*/
|
totalSeconds() {
|
return this._totalSeconds;
|
}
|
|
/**
|
*
|
* @returns {string}
|
*/
|
id() {
|
return this._id;
|
}
|
|
/**
|
*
|
* @param {number} totalSeconds
|
* @returns {string}
|
*/
|
static _buildId(totalSeconds) {
|
if (totalSeconds === 0) {
|
return 'Z';
|
} else {
|
const absTotalSeconds = Math.abs(totalSeconds);
|
const absHours = MathUtil.intDiv(absTotalSeconds, LocalTime.SECONDS_PER_HOUR);
|
const absMinutes = MathUtil.intMod(MathUtil.intDiv(absTotalSeconds, LocalTime.SECONDS_PER_MINUTE), LocalTime.MINUTES_PER_HOUR);
|
let buf = `${totalSeconds < 0 ? '-' : '+'
|
}${absHours < 10 ? '0' : ''}${absHours
|
}${absMinutes < 10 ? ':0' : ':'}${absMinutes}`;
|
const absSeconds = MathUtil.intMod(absTotalSeconds, LocalTime.SECONDS_PER_MINUTE);
|
if (absSeconds !== 0) {
|
buf += (absSeconds < 10 ? ':0' : ':') + (absSeconds);
|
}
|
return buf;
|
}
|
}
|
|
|
/**
|
*
|
* @param {number} totalSeconds
|
* @private
|
*/
|
static _validateTotalSeconds(totalSeconds){
|
if (Math.abs(totalSeconds) > ZoneOffset.MAX_SECONDS) {
|
throw new DateTimeException('Zone offset not in valid range: -18:00 to +18:00');
|
}
|
}
|
|
/**
|
*
|
* @param {number} hours
|
* @param {number} minutes
|
* @param {number} seconds
|
* @private
|
*/
|
static _validate(hours, minutes, seconds) {
|
if (hours < -18 || hours > 18) {
|
throw new DateTimeException(`Zone offset hours not in valid range: value ${hours
|
} is not in the range -18 to 18`);
|
}
|
if (hours > 0) {
|
if (minutes < 0 || seconds < 0) {
|
throw new DateTimeException('Zone offset minutes and seconds must be positive because hours is positive');
|
}
|
} else if (hours < 0) {
|
if (minutes > 0 || seconds > 0) {
|
throw new DateTimeException('Zone offset minutes and seconds must be negative because hours is negative');
|
}
|
} else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0)) {
|
throw new DateTimeException('Zone offset minutes and seconds must have the same sign');
|
}
|
if (Math.abs(minutes) > 59) {
|
throw new DateTimeException(`Zone offset minutes not in valid range: abs(value) ${
|
Math.abs(minutes)} is not in the range 0 to 59`);
|
}
|
if (Math.abs(seconds) > 59) {
|
throw new DateTimeException(`Zone offset seconds not in valid range: abs(value) ${
|
Math.abs(seconds)} is not in the range 0 to 59`);
|
}
|
if (Math.abs(hours) === 18 && (Math.abs(minutes) > 0 || Math.abs(seconds) > 0)) {
|
throw new DateTimeException('Zone offset not in valid range: -18:00 to +18:00');
|
}
|
}
|
|
//-----------------------------------------------------------------------
|
/**
|
* Obtains an instance of {@link ZoneOffset} using the ID.
|
*
|
* This method parses the string ID of a {@link ZoneOffset} to
|
* return an instance. The parsing accepts all the formats generated by
|
* {@link getId}, plus some additional formats:
|
*
|
* * {@link Z} - for UTC
|
* * `+h`
|
* * `+hh`
|
* * `+hh:mm`
|
* * `-hh:mm`
|
* * `+hhmm`
|
* * `-hhmm`
|
* * `+hh:mm:ss`
|
* * `-hh:mm:ss`
|
* * `+hhmmss`
|
* * `-hhmmss`
|
*
|
* Note that ± means either the plus or minus symbol.
|
*
|
* The ID of the returned offset will be normalized to one of the formats
|
* described by {@link getId}.
|
*
|
* The maximum supported range is from +18:00 to -18:00 inclusive.
|
*
|
* @param {string} offsetId the offset ID, not null
|
* @return {ZoneOffset} the zone-offset, not null
|
* @throws DateTimeException if the offset ID is invalid
|
*/
|
static of(offsetId) {
|
requireNonNull(offsetId, 'offsetId');
|
// "Z" is always in the cache
|
const offset = ID_CACHE[offsetId];
|
if (offset != null) {
|
return offset;
|
}
|
|
// parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss
|
let hours, minutes, seconds;
|
switch (offsetId.length) {
|
case 2:
|
offsetId = `${offsetId[0]}0${offsetId[1]}`; // fallthru
|
// eslint-disable-next-line no-fallthrough
|
case 3:
|
hours = ZoneOffset._parseNumber(offsetId, 1, false);
|
minutes = 0;
|
seconds = 0;
|
break;
|
case 5:
|
hours = ZoneOffset._parseNumber(offsetId, 1, false);
|
minutes = ZoneOffset._parseNumber(offsetId, 3, false);
|
seconds = 0;
|
break;
|
case 6:
|
hours = ZoneOffset._parseNumber(offsetId, 1, false);
|
minutes = ZoneOffset._parseNumber(offsetId, 4, true);
|
seconds = 0;
|
break;
|
case 7:
|
hours = ZoneOffset._parseNumber(offsetId, 1, false);
|
minutes = ZoneOffset._parseNumber(offsetId, 3, false);
|
seconds = ZoneOffset._parseNumber(offsetId, 5, false);
|
break;
|
case 9:
|
hours = ZoneOffset._parseNumber(offsetId, 1, false);
|
minutes = ZoneOffset._parseNumber(offsetId, 4, true);
|
seconds = ZoneOffset._parseNumber(offsetId, 7, true);
|
break;
|
default:
|
throw new DateTimeException(`Invalid ID for ZoneOffset, invalid format: ${offsetId}`);
|
}
|
const first = offsetId[0];
|
if (first !== '+' && first !== '-') {
|
throw new DateTimeException(`Invalid ID for ZoneOffset, plus/minus not found when expected: ${offsetId}`);
|
}
|
if (first === '-') {
|
return ZoneOffset.ofHoursMinutesSeconds(-hours, -minutes, -seconds);
|
} else {
|
return ZoneOffset.ofHoursMinutesSeconds(hours, minutes, seconds);
|
}
|
}
|
|
/**
|
* Parse a two digit zero-prefixed number.
|
*
|
* @param {string} offsetId - the offset ID, not null
|
* @param {number} pos - the position to parse, valid
|
* @param {boolean} precededByColon - should this number be prefixed by a precededByColon
|
* @return {number} the parsed number, from 0 to 99
|
*/
|
static _parseNumber(offsetId, pos, precededByColon) {
|
if (precededByColon && offsetId[pos - 1] !== ':') {
|
throw new DateTimeException(`Invalid ID for ZoneOffset, colon not found when expected: ${offsetId}`);
|
}
|
const ch1 = offsetId[pos];
|
const ch2 = offsetId[pos + 1];
|
if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') {
|
throw new DateTimeException(`Invalid ID for ZoneOffset, non numeric characters found: ${offsetId}`);
|
}
|
return (ch1.charCodeAt(0) - 48) * 10 + (ch2.charCodeAt(0) - 48);
|
}
|
|
/**
|
*
|
* @param {number} hours
|
* @returns {ZoneOffset}
|
*/
|
static ofHours(hours) {
|
return ZoneOffset.ofHoursMinutesSeconds(hours, 0, 0);
|
}
|
|
/**
|
*
|
* @param {number} hours
|
* @param {number} minutes
|
* @returns {ZoneOffset}
|
*/
|
static ofHoursMinutes(hours, minutes) {
|
return ZoneOffset.ofHoursMinutesSeconds(hours, minutes, 0);
|
}
|
|
/**
|
*
|
* @param {number} hours
|
* @param {number} minutes
|
* @param {number} seconds
|
* @returns {ZoneOffset}
|
*/
|
static ofHoursMinutesSeconds(hours, minutes, seconds) {
|
ZoneOffset._validate(hours, minutes, seconds);
|
const totalSeconds = hours * LocalTime.SECONDS_PER_HOUR + minutes * LocalTime.SECONDS_PER_MINUTE + seconds;
|
return ZoneOffset.ofTotalSeconds(totalSeconds);
|
}
|
|
/**
|
*
|
* @param {number} totalMinutes
|
* @returns {ZoneOffset}
|
*/
|
static ofTotalMinutes(totalMinutes) {
|
const totalSeconds = totalMinutes * LocalTime.SECONDS_PER_MINUTE;
|
return ZoneOffset.ofTotalSeconds(totalSeconds);
|
}
|
|
/**
|
*
|
* @param {number} totalSeconds
|
* @returns {ZoneOffset}
|
*/
|
static ofTotalSeconds(totalSeconds) {
|
if (totalSeconds % (15 * LocalTime.SECONDS_PER_MINUTE) === 0) {
|
const totalSecs = totalSeconds;
|
let result = SECONDS_CACHE[totalSecs];
|
if (result == null) {
|
result = new ZoneOffset(totalSeconds);
|
SECONDS_CACHE[totalSecs] = result;
|
ID_CACHE[result.id()] = result;
|
}
|
return result;
|
} else {
|
return new ZoneOffset(totalSeconds);
|
}
|
}
|
|
/**
|
* Gets the associated time-zone rules.
|
*
|
* The rules will always return this offset when queried.
|
* The implementation class is immutable, thread-safe and serializable.
|
*
|
* @return {ZoneRules} the rules, not null
|
*/
|
rules() {
|
return this._rules;
|
}
|
|
/**
|
* Gets the value of the specified field from this offset as an `int`.
|
*
|
* This queries this offset for the value for the specified field.
|
* The returned value will always be within the valid range of values for the field.
|
* If it is not possible to return the value, because the field is not supported
|
* or for some other reason, an exception is thrown.
|
*
|
* If the field is a {@link ChronoField} then the query is implemented here.
|
* The {@link OFFSET_SECONDS} field returns the value of the offset.
|
* All other {@link ChronoField} instances will throw a {@link DateTimeException}.
|
*
|
* If the field is not a {@link ChronoField}, then the result of this method
|
* is obtained by invoking {@link TemporalField.getFrom}
|
* passing `this` as the argument. Whether the value can be obtained,
|
* and what the value represents, is determined by the field.
|
*
|
* @param {TemporalField} field - the field to get, not null
|
* @return {number} the value for the field
|
* @throws DateTimeException if a value for the field cannot be obtained
|
* @throws ArithmeticException if numeric overflow occurs
|
*/
|
get(field) {
|
return this.getLong(field);
|
}
|
|
/**
|
* Gets the value of the specified field from this offset as a `long`.
|
*
|
* This queries this offset for the value for the specified field.
|
* If it is not possible to return the value, because the field is not supported
|
* or for some other reason, an exception is thrown.
|
*
|
* If the field is a {@link ChronoField} then the query is implemented here.
|
* The {@link OFFSET_SECONDS} field returns the value of the offset.
|
* All other {@link ChronoField} instances will throw a {@link DateTimeException}.
|
*
|
* If the field is not a {@link ChronoField}, then the result of this method
|
* is obtained by invoking {@link TemporalField.getFrom}
|
* passing `this` as the argument. Whether the value can be obtained,
|
* and what the value represents, is determined by the field.
|
*
|
* @param {TemporalField} field - the field to get, not null
|
* @return {number} the value for the field
|
* @throws DateTimeException if a value for the field cannot be obtained
|
* @throws ArithmeticException if numeric overflow occurs
|
*/
|
getLong(field) {
|
if (field === ChronoField.OFFSET_SECONDS) {
|
return this._totalSeconds;
|
} else if (field instanceof ChronoField) {
|
throw new DateTimeException(`Unsupported field: ${field}`);
|
}
|
return field.getFrom(this);
|
}
|
|
//-----------------------------------------------------------------------
|
/**
|
* Queries this offset using the specified query.
|
*
|
* This queries this offset using the specified query strategy object.
|
* The {@link TemporalQuery} object defines the logic to be used to
|
* obtain the result. Read the documentation of the query to understand
|
* what the result of this method will be.
|
*
|
* The result of this method is obtained by invoking the
|
* {@link TemporalQuery#queryFrom} method on the
|
* specified query passing `this` as the argument.
|
*
|
* @param {TemporalQuery} query - the query to invoke, not null
|
* @return {*} the query result, null may be returned (defined by the query)
|
* @throws DateTimeException if unable to query (defined by the query)
|
* @throws ArithmeticException if numeric overflow occurs (defined by the query)
|
*/
|
query(query) {
|
requireNonNull(query, 'query');
|
if (query === TemporalQueries.offset() || query === TemporalQueries.zone()) {
|
return this;
|
} else if (query === TemporalQueries.localDate() || query === TemporalQueries.localTime() ||
|
query === TemporalQueries.precision() || query === TemporalQueries.chronology() || query === TemporalQueries.zoneId()) {
|
return null;
|
}
|
return query.queryFrom(this);
|
}
|
|
/**
|
* Adjusts the specified temporal object to have the same offset as this object.
|
*
|
* This returns a temporal object of the same observable type as the input
|
* with the offset changed to be the same as this.
|
*
|
* The adjustment is equivalent to using {@link Temporal#with}
|
* passing {@link ChronoField#OFFSET_SECONDS} as the field.
|
*
|
* In most cases, it is clearer to reverse the calling pattern by using
|
* {@link Temporal#with}:
|
* <pre>
|
* // these two lines are equivalent, but the second approach is recommended
|
* temporal = thisOffset.adjustInto(temporal);
|
* temporal = temporal.with(thisOffset);
|
* </pre>
|
*
|
* This instance is immutable and unaffected by this method call.
|
*
|
* @param {Temporal} temporal - the target object to be adjusted, not null
|
* @return {Temporal} the adjusted object, not null
|
* @throws DateTimeException if unable to make the adjustment
|
* @throws ArithmeticException if numeric overflow occurs
|
*/
|
adjustInto(temporal) {
|
return temporal.with(ChronoField.OFFSET_SECONDS, this._totalSeconds);
|
}
|
|
/**
|
* Compares this offset to another offset in descending order.
|
*
|
* The offsets are compared in the order that they occur for the same time
|
* of day around the world. Thus, an offset of `+10:00` comes before an
|
* offset of `+09:00` and so on down to `-18:00`.
|
*
|
* The comparison is "consistent with equals", as defined by {@link Comparable}.
|
*
|
* @param {!ZoneOffset} other - the other date to compare to, not null
|
* @return {number} the comparator value, negative if less, positive if greater
|
* @throws NullPointerException if {@link other} is null
|
*/
|
compareTo(other) {
|
requireNonNull(other, 'other');
|
return other._totalSeconds - this._totalSeconds;
|
}
|
|
|
/**
|
* Checks if this offset is equal to another offset.
|
*
|
* The comparison is based on the amount of the offset in seconds.
|
* This is equivalent to a comparison by ID.
|
*
|
* @param {*} obj - the object to check, null returns false
|
* @return {boolean} true if this is equal to the other offset
|
*/
|
equals(obj) {
|
if (this === obj) {
|
return true;
|
}
|
if (obj instanceof ZoneOffset) {
|
return this._totalSeconds === obj._totalSeconds;
|
}
|
return false;
|
}
|
|
/**
|
* @return {number}
|
*/
|
hashCode(){
|
return this._totalSeconds;
|
}
|
|
/**
|
*
|
* @returns {string}
|
*/
|
toString(){
|
return this._id;
|
}
|
}
|
|
export function _init() {
|
ZoneOffset.MAX_SECONDS = 18 * LocalTime.SECONDS_PER_HOUR;
|
ZoneOffset.UTC = ZoneOffset.ofTotalSeconds(0);
|
ZoneOffset.MIN = ZoneOffset.ofTotalSeconds(-ZoneOffset.MAX_SECONDS);
|
ZoneOffset.MAX = ZoneOffset.ofTotalSeconds(ZoneOffset.MAX_SECONDS);
|
}
|