src/ZoneOffset.js
/**
* @copyright (c) 2016, Philipp Thuerwaechter & Pattrick Hueper
* @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';
var SECONDS_CACHE = {};
var ID_CACHE = {};
/**
*
* <h3>Static properties of Class {@link LocalDate}</h3>
*
* 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
*/
constructor(totalSeconds){
super();
ZoneOffset._validateTotalSeconds(totalSeconds);
this._totalSeconds = 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 {
var absTotalSeconds = Math.abs(totalSeconds);
var absHours = MathUtil.intDiv(absTotalSeconds, LocalTime.SECONDS_PER_HOUR);
var absMinutes = MathUtil.intMod(MathUtil.intDiv(absTotalSeconds, LocalTime.SECONDS_PER_MINUTE), LocalTime.MINUTES_PER_HOUR);
var buf = '' + (totalSeconds < 0 ? '-' : '+')
+ (absHours < 10 ? '0' : '') + (absHours)
+ (absMinutes < 10 ? ':0' : ':') + (absMinutes);
var 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 {@code ZoneOffset} using the ID.
* <p>
* This method parses the string ID of a {@code ZoneOffset} to
* return an instance. The parsing accepts all the formats generated by
* {@link #getId()}, plus some additional formats:
* <p><ul>
* <li>{@code Z} - for UTC
* <li>{@code +h}
* <li>{@code +hh}
* <li>{@code +hh:mm}
* <li>{@code -hh:mm}
* <li>{@code +hhmm}
* <li>{@code -hhmm}
* <li>{@code +hh:mm:ss}
* <li>{@code -hh:mm:ss}
* <li>{@code +hhmmss}
* <li>{@code -hhmmss}
* </ul><p>
* Note that ± means either the plus or minus symbol.
* <p>
* The ID of the returned offset will be normalized to one of the formats
* described by {@link #getId()}.
* <p>
* 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
var offset = ID_CACHE[offsetId];
if (offset != null) {
return offset;
}
// parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss
var hours, minutes, seconds;
switch (offsetId.length) {
case 2:
offsetId = offsetId[0] + '0' + offsetId[1]; // fallthru
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);
}
var 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);
}
var ch1 = offsetId[pos];
var 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);
var totalSeconds = hours * LocalTime.SECONDS_PER_HOUR + minutes * LocalTime.SECONDS_PER_MINUTE + seconds;
return ZoneOffset.ofTotalSeconds(totalSeconds);
}
/**
*
* @param {number} totalMinutes
* @returns {ZoneOffset}
*/
static ofTotalMinutes(totalMinutes) {
var 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) {
var totalSecs = totalSeconds;
var 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.
* <p>
* 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 {@code int}.
* <p>
* 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.
* <p>
* If the field is a {@link ChronoField} then the query is implemented here.
* The {@code OFFSET_SECONDS} field returns the value of the offset.
* All other {@code ChronoField} instances will throw a {@code DateTimeException}.
* <p>
* If the field is not a {@code ChronoField}, then the result of this method
* is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
* passing {@code 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 {@code long}.
* <p>
* 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.
* <p>
* If the field is a {@link ChronoField} then the query is implemented here.
* The {@code OFFSET_SECONDS} field returns the value of the offset.
* All other {@code ChronoField} instances will throw a {@code DateTimeException}.
* <p>
* If the field is not a {@code ChronoField}, then the result of this method
* is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
* passing {@code 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.
* <p>
* This queries this offset using the specified query strategy object.
* The {@code 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.
* <p>
* The result of this method is obtained by invoking the
* {@link TemporalQuery#queryFrom(TemporalAccessor)} method on the
* specified query passing {@code 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.
* <p>
* This returns a temporal object of the same observable type as the input
* with the offset changed to be the same as this.
* <p>
* The adjustment is equivalent to using {@link Temporal#with(TemporalField, long)}
* passing {@link ChronoField#OFFSET_SECONDS} as the field.
* <p>
* In most cases, it is clearer to reverse the calling pattern by using
* {@link Temporal#with(TemporalAdjuster)}:
* <pre>
* // these two lines are equivalent, but the second approach is recommended
* temporal = thisOffset.adjustInto(temporal);
* temporal = temporal.with(thisOffset);
* </pre>
* <p>
* 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.
* <p>
* The offsets are compared in the order that they occur for the same time
* of day around the world. Thus, an offset of {@code +10:00} comes before an
* offset of {@code +09:00} and so on down to {@code -18:00}.
* <p>
* 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, postive if greater
* @throws NullPointerException if {@code 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}
*/
id() {
return this._id;
}
/**
*
* @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);
}