1 /*
2  * hunt-time: A time library for D programming language.
3  *
4  * Copyright (C) 2015-2018 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.time.ZoneOffset;
13 
14 import hunt.time.LocalTime;
15 import hunt.time.temporal.ChronoField;
16 
17 import hunt.stream.DataInput;
18 import hunt.stream.DataOutput;
19 import hunt.Exceptions;
20 
21 import hunt.stream.Common;
22 import hunt.time.temporal.ChronoField;
23 import hunt.time.temporal.Temporal;
24 import hunt.time.temporal.TemporalAccessor;
25 import hunt.time.temporal.TemporalAdjuster;
26 import hunt.time.temporal.TemporalField;
27 import hunt.time.temporal.TemporalQueries;
28 import hunt.time.temporal.TemporalQuery;
29 import hunt.time.Exceptions;
30 import hunt.time.temporal.ValueRange;
31 import hunt.time.zone.ZoneRules;
32 import hunt.time.ZoneId;
33 import hunt.Functions;
34 import hunt.Integer;
35 import hunt.math.Helper;
36 import hunt.collection;
37 import hunt.text.Common;
38 import hunt.util.Common;
39 import hunt.time.Exceptions;
40 import hunt.util.StringBuilder;
41 import hunt.time.Ser;
42 import hunt.time.util.QueryHelper;
43 import hunt.time.util.Common;
44 
45 import std.concurrency: initOnce;
46 import std.conv;
47 
48 /**
49  * A time-zone offset from Greenwich/UTC, such as {@code +02:00}.
50  * !(p)
51  * A time-zone offset is the amount of time that a time-zone differs from Greenwich/UTC.
52  * This is usually a fixed number of hours and minutes.
53  * !(p)
54  * Different parts of the world have different time-zone offsets.
55  * The rules for how offsets vary by place and time of year are captured _in the
56  * {@link ZoneId} class.
57  * !(p)
58  * For example, Paris is one hour ahead of Greenwich/UTC _in winter and two hours
59  * ahead _in summer. The {@code ZoneId} instance for Paris will reference two
60  * {@code ZoneOffset} instances - a {@code +01:00} instance for winter,
61  * and a {@code +02:00} instance for summer.
62  * !(p)
63  * In 2008, time-zone offsets around the world extended from -12:00 to +14:00.
64  * To prevent any problems with that range being extended, yet still provide
65  * validation, the range of offsets is restricted to -18:00 to 18:00 inclusive.
66  * !(p)
67  * This class is designed for use with the ISO calendar system.
68  * The fields of hours, minutes and seconds make assumptions that are valid for the
69  * standard ISO definitions of those fields. This class may be used with other
70  * calendar systems providing the definition of the time fields matches those
71  * of the ISO calendar system.
72  * !(p)
73  * Instances of {@code ZoneOffset} must be compared using {@link #equals}.
74  * Implementations may choose to cache certain common offsets, however
75  * applications must not rely on such caching.
76  *
77  * !(p)
78  * This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
79  * class; use of identity-sensitive operations (including reference equality
80  * ({@code ==}), identity hash code, or synchronization) on instances of
81  * {@code ZoneOffset} may have unpredictable results and should be avoided.
82  * The {@code equals} method should be used for comparisons.
83  *
84  * @implSpec
85  * This class is immutable and thread-safe.
86  *
87  * @since 1.8
88  */
89 final class ZoneOffset : ZoneId, TemporalAccessor, TemporalAdjuster,
90     Comparable!(ZoneOffset) // , Serializable
91 {
92 
93     /** Cache of time-zone offset by offset _in seconds. */
94     // private static final ConcurrentMap!(Integer, ZoneOffset) SECONDS_CACHE = new ConcurrentHashMap!()(16, 0.75f, 4);
95     private static HashMap!(int, ZoneOffset) SECONDS_CACHE() {
96         __gshared HashMap!(int, ZoneOffset) z;
97         return initOnce!(z)(new HashMap!(int, ZoneOffset)(16, 0.75f));
98     } 
99 
100     /** Cache of time-zone offset by ID. */
101     //  static final ConcurrentMap!(string, ZoneOffset) ID_CACHE = new ConcurrentHashMap!()(16, 0.75f, 4);
102     static HashMap!(string, ZoneOffset) ID_CACHE() {
103         __gshared HashMap!(string, ZoneOffset) z;
104         return initOnce!(z)(new HashMap!(string, ZoneOffset)(16, 0.75f));
105     } 
106 
107     /**
108      * The abs maximum seconds.
109      */
110      enum int MAX_SECONDS = 18 * LocalTime.SECONDS_PER_HOUR;
111      
112      
113     /**
114      * The time-zone offset for UTC, with an ID of 'Z'.
115      */
116     static ZoneOffset UTC() {
117         __gshared ZoneOffset z;
118         return initOnce!(z)(ZoneOffset.ofTotalSeconds(0));
119     } 
120 
121     /**
122      * Constant for the minimum supported offset.
123      */
124     static ZoneOffset MIN() {
125         __gshared ZoneOffset _MIN;
126         return initOnce!(_MIN)(ZoneOffset.ofTotalSeconds(-MAX_SECONDS));
127     }
128 
129     /**
130      * Constant for the maximum supported offset.
131      */
132     static ZoneOffset MAX() {
133         __gshared ZoneOffset _MAX;
134         return initOnce!(_MAX)(ZoneOffset.ofTotalSeconds(MAX_SECONDS));
135     }
136     
137 
138     /**
139      * The total offset _in seconds.
140      */
141     private int _totalSeconds;
142 
143     /**
144      * The string form of the time-zone offset.
145      */
146     private  string id;
147 
148     //-----------------------------------------------------------------------
149     /**
150      * Obtains an instance of {@code ZoneOffset} using the ID.
151      * !(p)
152      * This method parses the string ID of a {@code ZoneOffset} to
153      * return an instance. The parsing accepts all the formats generated by
154      * {@link #getId()}, plus some additional formats:
155      * !(ul)
156      * !(li){@code Z} - for UTC
157      * !(li){@code +h}
158      * !(li){@code +hh}
159      * !(li){@code +hh:mm}
160      * !(li){@code -hh:mm}
161      * !(li){@code +hhmm}
162      * !(li){@code -hhmm}
163      * !(li){@code +hh:mm:ss}
164      * !(li){@code -hh:mm:ss}
165      * !(li){@code +hhmmss}
166      * !(li){@code -hhmmss}
167      * </ul>
168      * Note that &plusmn; means either the plus or minus symbol.
169      * !(p)
170      * The ID of the returned offset will be normalized to one of the formats
171      * described by {@link #getId()}.
172      * !(p)
173      * The maximum supported range is from +18:00 to -18:00 inclusive.
174      *
175      * @param offsetId  the offset ID, not null
176      * @return the zone-offset, not null
177      * @throws DateTimeException if the offset ID is invalid
178      */
179     // @SuppressWarnings("fallthrough")
180     static ZoneOffset of(string offsetId)
181     {
182         assert(offsetId, "offsetId");
183         // "Z" is always _in the cache
184         ZoneOffset offset = ID_CACHE.get(offsetId);
185         if (offset !is null)
186         {
187             return offset;
188         }
189 
190         // parse - +h, +hh, +hhmm, +hh:mm, +hhmmss, +hh:mm:ss
191         int hours, minutes, seconds;
192         switch (offsetId.length)
193         {
194         case 2:
195             offsetId = offsetId[0] ~ "0" ~ offsetId[1]; // fallthru
196             goto case 3;
197         case 3:
198             hours = parseNumber(offsetId, 1, false);
199             minutes = 0;
200             seconds = 0;
201             break;
202         case 5:
203             hours = parseNumber(offsetId, 1, false);
204             minutes = parseNumber(offsetId, 3, false);
205             seconds = 0;
206             break;
207         case 6:
208             hours = parseNumber(offsetId, 1, false);
209             minutes = parseNumber(offsetId, 4, true);
210             seconds = 0;
211             break;
212         case 7:
213             hours = parseNumber(offsetId, 1, false);
214             minutes = parseNumber(offsetId, 3, false);
215             seconds = parseNumber(offsetId, 5, false);
216             break;
217         case 9:
218             hours = parseNumber(offsetId, 1, false);
219             minutes = parseNumber(offsetId, 4, true);
220             seconds = parseNumber(offsetId, 7, true);
221             break;
222         default:
223             throw new DateTimeException("Invalid ID for ZoneOffset, invalid format: " ~ offsetId);
224         }
225         char first = offsetId[0];
226         if (first != '+' && first != '-')
227         {
228             throw new DateTimeException(
229                     "Invalid ID for ZoneOffset, plus/minus not found when expected: " ~ offsetId);
230         }
231         if (first == '-')
232         {
233             return ofHoursMinutesSeconds(-hours, -minutes, -seconds);
234         }
235         else
236         {
237             return ofHoursMinutesSeconds(hours, minutes, seconds);
238         }
239     }
240 
241     /**
242      * Parse a two digit zero-prefixed number.
243      *
244      * @param offsetId  the offset ID, not null
245      * @param pos  the position to parse, valid
246      * @param precededByColon  should this number be prefixed by a precededByColon
247      * @return the parsed number, from 0 to 99
248      */
249     private static int parseNumber(string offsetId, int pos, bool precededByColon)
250     {
251         if (precededByColon && offsetId[pos - 1] != ':')
252         {
253             throw new DateTimeException(
254                     "Invalid ID for ZoneOffset, colon not found when expected: " ~ offsetId);
255         }
256         char ch1 = offsetId[pos];
257         char ch2 = offsetId[pos + 1];
258         if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9')
259         {
260             throw new DateTimeException(
261                     "Invalid ID for ZoneOffset, non numeric characters found: " ~ offsetId);
262         }
263         return (ch1 - 48) * 10 + (ch2 - 48);
264     }
265 
266     //-----------------------------------------------------------------------
267     /**
268      * Obtains an instance of {@code ZoneOffset} using an offset _in hours.
269      *
270      * @param hours  the time-zone offset _in hours, from -18 to +18
271      * @return the zone-offset, not null
272      * @throws DateTimeException if the offset is not _in the required range
273      */
274     static ZoneOffset ofHours(int hours)
275     {
276         return ofHoursMinutesSeconds(hours, 0, 0);
277     }
278 
279     /**
280      * Obtains an instance of {@code ZoneOffset} using an offset _in
281      * hours and minutes.
282      * !(p)
283      * The sign of the hours and minutes components must match.
284      * Thus, if the hours is negative, the minutes must be negative or zero.
285      * If the hours is zero, the minutes may be positive, negative or zero.
286      *
287      * @param hours  the time-zone offset _in hours, from -18 to +18
288      * @param minutes  the time-zone offset _in minutes, from 0 to &plusmn;59, sign matches hours
289      * @return the zone-offset, not null
290      * @throws DateTimeException if the offset is not _in the required range
291      */
292     static ZoneOffset ofHoursMinutes(int hours, int minutes)
293     {
294         return ofHoursMinutesSeconds(hours, minutes, 0);
295     }
296 
297     /**
298      * Obtains an instance of {@code ZoneOffset} using an offset _in
299      * hours, minutes and seconds.
300      * !(p)
301      * The sign of the hours, minutes and seconds components must match.
302      * Thus, if the hours is negative, the minutes and seconds must be negative or zero.
303      *
304      * @param hours  the time-zone offset _in hours, from -18 to +18
305      * @param minutes  the time-zone offset _in minutes, from 0 to &plusmn;59, sign matches hours and seconds
306      * @param seconds  the time-zone offset _in seconds, from 0 to &plusmn;59, sign matches hours and minutes
307      * @return the zone-offset, not null
308      * @throws DateTimeException if the offset is not _in the required range
309      */
310     static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds)
311     {
312         validate(hours, minutes, seconds);
313         int _totalSeconds = totalSeconds(hours, minutes, seconds);
314         return ofTotalSeconds(_totalSeconds);
315     }
316 
317     //-----------------------------------------------------------------------
318     /**
319      * Obtains an instance of {@code ZoneOffset} from a temporal object.
320      * !(p)
321      * This obtains an offset based on the specified temporal.
322      * A {@code TemporalAccessor} represents an arbitrary set of date and time information,
323      * which this factory converts to an instance of {@code ZoneOffset}.
324      * !(p)
325      * A {@code TemporalAccessor} represents some form of date and time information.
326      * This factory converts the arbitrary temporal object to an instance of {@code ZoneOffset}.
327      * !(p)
328      * The conversion uses the {@link TemporalQueries#offset()} query, which relies
329      * on extracting the {@link ChronoField#OFFSET_SECONDS OFFSET_SECONDS} field.
330      * !(p)
331      * This method matches the signature of the functional interface {@link TemporalQuery}
332      * allowing it to be used as a query via method reference, {@code ZoneOffset::from}.
333      *
334      * @param temporal  the temporal object to convert, not null
335      * @return the zone-offset, not null
336      * @throws DateTimeException if unable to convert to an {@code ZoneOffset}
337      */
338     static ZoneOffset from(TemporalAccessor temporal)
339     {
340         assert(temporal, "temporal");
341         ZoneOffset offset = QueryHelper.query!ZoneOffset(temporal, TemporalQueries.offset());
342         if (offset is null)
343         {
344             throw new DateTimeException("Unable to obtain ZoneOffset from TemporalAccessor: " ~ typeid(temporal)
345                     .name ~ " of type " ~ typeid(temporal).stringof);
346         }
347         return offset;
348     }
349 
350     //-----------------------------------------------------------------------
351     /**
352      * Validates the offset fields.
353      *
354      * @param hours  the time-zone offset _in hours, from -18 to +18
355      * @param minutes  the time-zone offset _in minutes, from 0 to &plusmn;59
356      * @param seconds  the time-zone offset _in seconds, from 0 to &plusmn;59
357      * @throws DateTimeException if the offset is not _in the required range
358      */
359     private static void validate(int hours, int minutes, int seconds)
360     {
361         if (hours < -18 || hours > 18)
362         {
363             throw new DateTimeException(
364                     "Zone offset hours not _in valid range: value "
365                     ~ hours.to!string ~ " is not _in the range -18 to 18");
366         }
367         if (hours > 0)
368         {
369             if (minutes < 0 || seconds < 0)
370             {
371                 throw new DateTimeException(
372                         "Zone offset minutes and seconds must be positive because hours is positive");
373             }
374         }
375         else if (hours < 0)
376         {
377             if (minutes > 0 || seconds > 0)
378             {
379                 throw new DateTimeException(
380                         "Zone offset minutes and seconds must be negative because hours is negative");
381             }
382         }
383         else if ((minutes > 0 && seconds < 0) || (minutes < 0 && seconds > 0))
384         {
385             throw new DateTimeException("Zone offset minutes and seconds must have the same sign");
386         }
387         if (minutes < -59 || minutes > 59)
388         {
389             throw new DateTimeException(
390                     "Zone offset minutes not _in valid range: value "
391                     ~ minutes.to!string ~ " is not _in the range -59 to 59");
392         }
393         if (seconds < -59 || seconds > 59)
394         {
395             throw new DateTimeException(
396                     "Zone offset seconds not _in valid range: value "
397                     ~ seconds.to!string ~ " is not _in the range -59 to 59");
398         }
399         if (MathHelper.abs(hours) == 18 && (minutes | seconds) != 0)
400         {
401             throw new DateTimeException("Zone offset not _in valid range: -18:00 to +18:00");
402         }
403     }
404 
405     /**
406      * Calculates the total offset _in seconds.
407      *
408      * @param hours  the time-zone offset _in hours, from -18 to +18
409      * @param minutes  the time-zone offset _in minutes, from 0 to &plusmn;59, sign matches hours and seconds
410      * @param seconds  the time-zone offset _in seconds, from 0 to &plusmn;59, sign matches hours and minutes
411      * @return the total _in seconds
412      */
413     private static int totalSeconds(int hours, int minutes, int seconds)
414     {
415         return hours * LocalTime.SECONDS_PER_HOUR + minutes * LocalTime.SECONDS_PER_MINUTE + seconds;
416     }
417 
418     //-----------------------------------------------------------------------
419     /**
420      * Obtains an instance of {@code ZoneOffset} specifying the total offset _in seconds
421      * !(p)
422      * The offset must be _in the range {@code -18:00} to {@code +18:00}, which corresponds to -64800 to +64800.
423      *
424      * @param _totalSeconds  the total time-zone offset _in seconds, from -64800 to +64800
425      * @return the ZoneOffset, not null
426      * @throws DateTimeException if the offset is not _in the required range
427      */
428     static ZoneOffset ofTotalSeconds(int _totalSeconds)
429     {
430         if (_totalSeconds < -MAX_SECONDS || _totalSeconds > MAX_SECONDS)
431         {
432             throw new DateTimeException("Zone offset not _in valid range: -18:00 to +18:00");
433         }
434         if (_totalSeconds % (15 * LocalTime.SECONDS_PER_MINUTE) == 0)
435         {
436             // Integer totalSecs = new Integer(_totalSeconds);
437             ZoneOffset result;
438             if (SECONDS_CACHE.containsKey(_totalSeconds)) {
439                 result = SECONDS_CACHE.get(_totalSeconds);
440             } else {
441                 result = new ZoneOffset(_totalSeconds);
442                 SECONDS_CACHE.putIfAbsent(_totalSeconds, result);
443                 result = SECONDS_CACHE.get(_totalSeconds);
444                 ID_CACHE.putIfAbsent(result.getId(), result);
445             }
446             return result;
447         }
448         else
449         {
450             return new ZoneOffset(_totalSeconds);
451         }
452     }
453 
454     //-----------------------------------------------------------------------
455     /**
456      * Constructor.
457      *
458      * @param _totalSeconds  the total time-zone offset _in seconds, from -64800 to +64800
459      */
460     private this(int _totalSeconds)
461     {
462         super();
463         this._totalSeconds = _totalSeconds;
464         id = buildId(_totalSeconds);
465     }
466 
467     private static string buildId(int _totalSeconds)
468     {
469         if (_totalSeconds == 0)
470         {
471             return "Z";
472         }
473         else
474         {
475             int absTotalSeconds = MathHelper.abs(_totalSeconds);
476             StringBuilder buf = new StringBuilder();
477             int absHours = absTotalSeconds / LocalTime.SECONDS_PER_HOUR;
478             int absMinutes = (absTotalSeconds / LocalTime.SECONDS_PER_MINUTE) % LocalTime
479                 .MINUTES_PER_HOUR;
480             buf.append(_totalSeconds < 0 ? "-" : "+").append(absHours < 10 ? "0"
481                     : "").append(absHours).append(absMinutes < 10 ? ":0" : ":").append(absMinutes);
482             int absSeconds = absTotalSeconds % LocalTime.SECONDS_PER_MINUTE;
483             if (absSeconds != 0)
484             {
485                 buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds);
486             }
487             return buf.toString();
488         }
489     }
490 
491     //-----------------------------------------------------------------------
492     /**
493      * Gets the total zone offset _in seconds.
494      * !(p)
495      * This is the primary way to access the offset amount.
496      * It returns the total of the hours, minutes and seconds fields as a
497      * single offset that can be added to a time.
498      *
499      * @return the total zone offset amount _in seconds
500      */
501     int getTotalSeconds()
502     {
503         return _totalSeconds;
504     }
505 
506     /**
507      * Gets the normalized zone offset ID.
508      * !(p)
509      * The ID is minor variation to the standard ISO-8601 formatted string
510      * for the offset. There are three formats:
511      * !(ul)
512      * !(li){@code Z} - for UTC (ISO-8601)
513      * !(li){@code +hh:mm} or {@code -hh:mm} - if the seconds are zero (ISO-8601)
514      * !(li){@code +hh:mm:ss} or {@code -hh:mm:ss} - if the seconds are non-zero (not ISO-8601)
515      * </ul>
516      *
517      * @return the zone offset ID, not null
518      */
519     override string getId()
520     {
521         return id;
522     }
523 
524     /**
525      * Gets the associated time-zone rules.
526      * !(p)
527      * The rules will always return this offset when queried.
528      * The implementation class is immutable, thread-safe and serializable.
529      *
530      * @return the rules, not null
531      */
532     override ZoneRules getRules()
533     {
534         return ZoneRules.of(this);
535     }
536 
537     //-----------------------------------------------------------------------
538     /**
539      * Checks if the specified field is supported.
540      * !(p)
541      * This checks if this offset can be queried for the specified field.
542      * If false, then calling the {@link #range(TemporalField) range} and
543      * {@link #get(TemporalField) get} methods will throw an exception.
544      * !(p)
545      * If the field is a {@link ChronoField} then the query is implemented here.
546      * The {@code OFFSET_SECONDS} field returns true.
547      * All other {@code ChronoField} instances will return false.
548      * !(p)
549      * If the field is not a {@code ChronoField}, then the result of this method
550      * is obtained by invoking {@code TemporalField.isSupportedBy(TemporalAccessor)}
551      * passing {@code this} as the argument.
552      * Whether the field is supported is determined by the field.
553      *
554      * @param field  the field to check, null returns false
555      * @return true if the field is supported on this offset, false if not
556      */
557     override bool isSupported(TemporalField field)
558     {
559         if (cast(ChronoField)(field) !is null)
560         {
561             return field == ChronoField.OFFSET_SECONDS;
562         }
563         return field !is null && field.isSupportedBy(this);
564     }
565 
566     /**
567      * Gets the range of valid values for the specified field.
568      * !(p)
569      * The range object expresses the minimum and maximum valid values for a field.
570      * This offset is used to enhance the accuracy of the returned range.
571      * If it is not possible to return the range, because the field is not supported
572      * or for some other reason, an exception is thrown.
573      * !(p)
574      * If the field is a {@link ChronoField} then the query is implemented here.
575      * The {@link #isSupported(TemporalField) supported fields} will return
576      * appropriate range instances.
577      * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}.
578      * !(p)
579      * If the field is not a {@code ChronoField}, then the result of this method
580      * is obtained by invoking {@code TemporalField.rangeRefinedBy(TemporalAccessor)}
581      * passing {@code this} as the argument.
582      * Whether the range can be obtained is determined by the field.
583      *
584      * @param field  the field to query the range for, not null
585      * @return the range of valid values for the field, not null
586      * @throws DateTimeException if the range for the field cannot be obtained
587      * @throws UnsupportedTemporalTypeException if the field is not supported
588      */
589     override  // override for Javadoc
590     ValueRange range(TemporalField field)
591     {
592         return  /* TemporalAccessor. super.*/ super_range(field);
593     }
594 
595     ValueRange super_range(TemporalField field)
596     {
597         if (cast(ChronoField)(field) !is null)
598         {
599             if (isSupported(field))
600             {
601                 return field.range();
602             }
603             throw new UnsupportedTemporalTypeException("Unsupported field: " ~ typeid(field).name);
604         }
605         assert(field, "field");
606         return field.rangeRefinedBy(this);
607     }
608     /**
609      * Gets the value of the specified field from this offset as an {@code int}.
610      * !(p)
611      * This queries this offset for the value of the specified field.
612      * The returned value will always be within the valid range of values for the field.
613      * If it is not possible to return the value, because the field is not supported
614      * or for some other reason, an exception is thrown.
615      * !(p)
616      * If the field is a {@link ChronoField} then the query is implemented here.
617      * The {@code OFFSET_SECONDS} field returns the value of the offset.
618      * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}.
619      * !(p)
620      * If the field is not a {@code ChronoField}, then the result of this method
621      * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
622      * passing {@code this} as the argument. Whether the value can be obtained,
623      * and what the value represents, is determined by the field.
624      *
625      * @param field  the field to get, not null
626      * @return the value for the field
627      * @throws DateTimeException if a value for the field cannot be obtained or
628      *         the value is outside the range of valid values for the field
629      * @throws UnsupportedTemporalTypeException if the field is not supported or
630      *         the range of values exceeds an {@code int}
631      * @throws ArithmeticException if numeric overflow occurs
632      */
633     override  // override for Javadoc and performance
634     int get(TemporalField field)
635     {
636         if (field == ChronoField.OFFSET_SECONDS)
637         {
638             return _totalSeconds;
639         }
640         else if (cast(ChronoField)(field) !is null)
641         {
642             throw new UnsupportedTemporalTypeException("Unsupported field: " ~ typeid(field).name);
643         }
644         return range(field).checkValidIntValue(getLong(field), field);
645     }
646 
647     /**
648      * Gets the value of the specified field from this offset as a {@code long}.
649      * !(p)
650      * This queries this offset for the value of the specified field.
651      * If it is not possible to return the value, because the field is not supported
652      * or for some other reason, an exception is thrown.
653      * !(p)
654      * If the field is a {@link ChronoField} then the query is implemented here.
655      * The {@code OFFSET_SECONDS} field returns the value of the offset.
656      * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}.
657      * !(p)
658      * If the field is not a {@code ChronoField}, then the result of this method
659      * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)}
660      * passing {@code this} as the argument. Whether the value can be obtained,
661      * and what the value represents, is determined by the field.
662      *
663      * @param field  the field to get, not null
664      * @return the value for the field
665      * @throws DateTimeException if a value for the field cannot be obtained
666      * @throws UnsupportedTemporalTypeException if the field is not supported
667      * @throws ArithmeticException if numeric overflow occurs
668      */
669     override long getLong(TemporalField field)
670     {
671         if (field == ChronoField.OFFSET_SECONDS)
672         {
673             return _totalSeconds;
674         }
675         else if (cast(ChronoField)(field) !is null)
676         {
677             throw new UnsupportedTemporalTypeException("Unsupported field: " ~ typeid(field).name);
678         }
679         return field.getFrom(this);
680     }
681 
682     //-----------------------------------------------------------------------
683     /**
684      * Queries this offset using the specified query.
685      * !(p)
686      * This queries this offset using the specified query strategy object.
687      * The {@code TemporalQuery} object defines the logic to be used to
688      * obtain the result. Read the documentation of the query to understand
689      * what the result of this method will be.
690      * !(p)
691      * The result of this method is obtained by invoking the
692      * {@link TemporalQuery#queryFrom(TemporalAccessor)} method on the
693      * specified query passing {@code this} as the argument.
694      *
695      * @param !(R) the type of the result
696      * @param query  the query to invoke, not null
697      * @return the query result, null may be returned (defined by the query)
698      * @throws DateTimeException if unable to query (defined by the query)
699      * @throws ArithmeticException if numeric overflow occurs (defined by the query)
700      */
701     /*@SuppressWarnings("unchecked")*/
702     /* override */ R query(R)(TemporalQuery!(R) query)
703     {
704         if (query == TemporalQueries.offset() || query == TemporalQueries.zone())
705         {
706             return cast(R) this;
707         }
708         return  /* TemporalAccessor. */ super_query(query);
709     }
710     R super_query(R)(TemporalQuery!(R) query) {
711          if (query == TemporalQueries.zoneId()
712                  || query == TemporalQueries.chronology()
713                  || query == TemporalQueries.precision()) {
714              return null;
715          }
716          return query.queryFrom(this);
717      }
718     /**
719      * Adjusts the specified temporal object to have the same offset as this object.
720      * !(p)
721      * This returns a temporal object of the same observable type as the input
722      * with the offset changed to be the same as this.
723      * !(p)
724      * The adjustment is equivalent to using {@link Temporal#_with(TemporalField, long)}
725      * passing {@link ChronoField#OFFSET_SECONDS} as the field.
726      * !(p)
727      * In most cases, it is clearer to reverse the calling pattern by using
728      * {@link Temporal#_with(TemporalAdjuster)}:
729      * !(pre)
730      *   // these two lines are equivalent, but the second approach is recommended
731      *   temporal = thisOffset.adjustInto(temporal);
732      *   temporal = temporal._with(thisOffset);
733      * </pre>
734      * !(p)
735      * This instance is immutable and unaffected by this method call.
736      *
737      * @param temporal  the target object to be adjusted, not null
738      * @return the adjusted object, not null
739      * @throws DateTimeException if unable to make the adjustment
740      * @throws ArithmeticException if numeric overflow occurs
741      */
742     override Temporal adjustInto(Temporal temporal)
743     {
744         return temporal._with(ChronoField.OFFSET_SECONDS, _totalSeconds);
745     }
746 
747     //-----------------------------------------------------------------------
748     /**
749      * Compares this offset to another offset _in descending order.
750      * !(p)
751      * The offsets are compared _in the order that they occur for the same time
752      * of day around the world. Thus, an offset of {@code +10:00} comes before an
753      * offset of {@code +09:00} and so on down to {@code -18:00}.
754      * !(p)
755      * The comparison is "consistent with equals", as defined by {@link Comparable}.
756      *
757      * @param other  the other date to compare to, not null
758      * @return the comparator value, negative if less, positive if greater
759      * @throws NullPointerException if {@code other} is null
760      */
761     // override
762     int compareTo(ZoneOffset other)
763     {
764         // abs(_totalSeconds) <= MAX_SECONDS, so no overflow can happen here
765         return other._totalSeconds - _totalSeconds;
766     }
767 
768     override int opCmp(ZoneOffset other)
769     {
770         // abs(_totalSeconds) <= MAX_SECONDS, so no overflow can happen here
771         return other._totalSeconds - _totalSeconds;
772     }
773 
774     //-----------------------------------------------------------------------
775     /**
776      * Checks if this offset is equal to another offset.
777      * !(p)
778      * The comparison is based on the amount of the offset _in seconds.
779      * This is equivalent to a comparison by ID.
780      *
781      * @param obj  the object to check, null returns false
782      * @return true if this is equal to the other offset
783      */
784     override bool opEquals(Object obj)
785     {
786         if (this is obj)
787         {
788             return true;
789         }
790         if (cast(ZoneOffset)(obj) !is null)
791         {
792             return _totalSeconds == (cast(ZoneOffset) obj)._totalSeconds;
793         }
794         return false;
795     }
796 
797     /**
798      * A hash code for this offset.
799      *
800      * @return a suitable hash code
801      */
802     override size_t toHash() @trusted nothrow
803     {
804         return _totalSeconds;
805     }
806 
807     //-----------------------------------------------------------------------
808     /**
809      * Outputs this offset as a {@code string}, using the normalized ID.
810      *
811      * @return a string representation of this offset, not null
812      */
813     override string toString()
814     {
815         return id;
816     }
817 
818     // -----------------------------------------------------------------------
819     /**
820      * Writes the object using a
821      * <a href="{@docRoot}/serialized-form.html#hunt.time.Ser">dedicated serialized form</a>.
822      * @serialData
823      * !(pre)
824      *  _out.writeByte(8);                  // identifies a ZoneOffset
825      *  int offsetByte = _totalSeconds % 900 == 0 ? _totalSeconds / 900 : 127;
826      *  _out.writeByte(offsetByte);
827      *  if (offsetByte == 127) {
828      *      _out.writeInt(_totalSeconds);
829      *  }
830      * </pre>
831      *
832      * @return the instance of {@code Ser}, not null
833      */
834     // private Object writeReplace()
835     // {
836     //     return new Ser(Ser.ZONE_OFFSET_TYPE, this);
837     // }
838 
839     /**
840      * Defend against malicious streams.
841      *
842      * @param s the stream to read
843      * @throws InvalidObjectException always
844      */
845     ///@gxc
846     // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {
847     //     throw new InvalidObjectException("Deserialization via serialization delegate");
848     // }
849 
850     // override void write(DataOutput _out) /*throws IOException*/
851     // {
852     //     _out.writeByte(Ser.ZONE_OFFSET_TYPE);
853     //     writeExternal(_out);
854     // }
855 
856     // void writeExternal(DataOutput _out) /*throws IOException*/
857     // {
858     //     int offsetSecs = _totalSeconds;
859     //     int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127; // compress to -72 to +72
860     //     _out.writeByte(offsetByte);
861     //     if (offsetByte == 127)
862     //     {
863     //         _out.writeInt(offsetSecs);
864     //     }
865     // }
866 
867     // static ZoneOffset readExternal(DataInput _in) /*throws IOException*/
868     // {
869     //     int offsetByte = _in.readByte();
870     //     return (offsetByte == 127 ? ZoneOffset.ofTotalSeconds(_in.readInt())
871     //             : ZoneOffset.ofTotalSeconds(offsetByte * 900));
872     // }
873 
874 }