wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
/**
 * @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 &plusmn; 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);
}