packages/core/src/ZoneOffset.js
/**
* @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);
}