1 module hunt.time.format.ZoneIdPrinterParser;
2 
3 import hunt.time.Exceptions;
4 
5 import hunt.time.format.DateTimeParseContext;
6 import hunt.time.format.DateTimePrinterParser;
7 import hunt.time.format.DateTimePrintContext;
8 import hunt.time.format.OffsetIdPrinterParser;
9 import hunt.time.format.PrefixTree;
10 import hunt.time.temporal.ChronoField;
11 import hunt.time.temporal.TemporalField;
12 import hunt.time.temporal.TemporalQuery;
13 import hunt.time.text.ParsePosition;
14 import hunt.time.util.Common;
15 import hunt.time.ZoneId;
16 import hunt.time.ZoneOffset;
17 import hunt.time.ZoneRegion;
18 import hunt.time.zone.ZoneRulesProvider;
19 import hunt.util.StringBuilder;
20 
21 import hunt.Exceptions;
22 import hunt.Integer;
23 import hunt.collection.AbstractMap;
24 import hunt.collection.Map;
25 import hunt.collection.Set;
26 
27 import std.string;
28 
29 
30 //-----------------------------------------------------------------------
31 /**
32 * Prints or parses a zone ID.
33 */
34 static class ZoneIdPrinterParser : DateTimePrinterParser
35 {
36     private TemporalQuery!(ZoneId) query;
37     private string description;
38 
39     this(TemporalQuery!(ZoneId) query, string description)
40     {
41         this.query = query;
42         this.description = description;
43     }
44 
45     override public bool format(DateTimePrintContext context, StringBuilder buf)
46     {
47         ZoneId zone = context.getValue(query);
48         if (zone is null)
49         {
50             return false;
51         }
52         buf.append(zone.getId());
53         return true;
54     }
55 
56     /**
57  * The cached tree to speed up parsing.
58  */
59     private __gshared MapEntry!(Integer, PrefixTree) cachedPrefixTree;
60     private __gshared MapEntry!(Integer, PrefixTree) cachedPrefixTreeCI;
61 
62     protected PrefixTree getTree(DateTimeParseContext context)
63     {
64         // prepare parse tree
65         Set!(string) regionIds = ZoneRulesProvider.getAvailableZoneIds();
66         int regionIdsSize = regionIds.size();
67         MapEntry!(Integer, PrefixTree) cached = context.isCaseSensitive()
68             ? cachedPrefixTree : cachedPrefixTreeCI;
69         if (cached is null || cached.getKey() != regionIdsSize)
70         {
71             synchronized (this)
72             {
73                 cached = context.isCaseSensitive() ? cachedPrefixTree : cachedPrefixTreeCI;
74                 if (cached is null || cached.getKey() != regionIdsSize)
75                 {
76                     cached = new SimpleImmutableEntry!(Integer, PrefixTree)(new Integer(regionIdsSize),
77                             PrefixTree.newTree(regionIds, context));
78                     if (context.isCaseSensitive())
79                     {
80                         cachedPrefixTree = cached;
81                     }
82                     else
83                     {
84                         cachedPrefixTreeCI = cached;
85                     }
86                 }
87             }
88         }
89         return cached.getValue();
90     }
91 
92     /**
93  * This implementation looks for the longest matching string.
94  * For example, parsing Etc/GMT-2 will return Etc/GMC-2 rather than just
95  * Etc/GMC although both are valid.
96  */
97     override public int parse(DateTimeParseContext context, string text, int position)
98     {
99         int length = cast(int)(text.length);
100         if (position > length)
101         {
102             throw new IndexOutOfBoundsException();
103         }
104         if (position == length)
105         {
106             return ~position;
107         }
108 
109         // handle fixed time-zone IDs
110         char nextChar = text[position];
111         if (nextChar == '+' || nextChar == '-')
112         {
113             return parseOffsetBased(context, text, position, position,
114                     OffsetIdPrinterParser.INSTANCE_ID_Z);
115         }
116         else if (length >= position + 2)
117         {
118             char nextNextChar = text[position + 1];
119             if (context.charEquals(nextChar, 'U') && context.charEquals(nextNextChar, 'T'))
120             {
121                 if (length >= position + 3
122                         && context.charEquals(text[position + 2], 'C'))
123                 {
124                     return parseOffsetBased(context, text, position,
125                             position + 3, OffsetIdPrinterParser.INSTANCE_ID_ZERO);
126                 }
127                 return parseOffsetBased(context, text, position,
128                         position + 2, OffsetIdPrinterParser.INSTANCE_ID_ZERO);
129             }
130             else if (context.charEquals(nextChar, 'G') && length >= position + 3
131                     && context.charEquals(nextNextChar, 'M')
132                     && context.charEquals(text[position + 2], 'T'))
133             {
134                 if (length >= position + 4
135                         && context.charEquals(text[position + 3], '0'))
136                 {
137                     context.setParsed(ZoneRegion.of("GMT0"));
138                     return position + 4;
139                 }
140                 return parseOffsetBased(context, text, position,
141                         position + 3, OffsetIdPrinterParser.INSTANCE_ID_ZERO);
142             }
143         }
144 
145         // parse
146         PrefixTree tree = getTree(context);
147         ParsePosition ppos = new ParsePosition(position);
148         string parsedZoneId = tree.match(text, ppos);
149         if (parsedZoneId is null)
150         {
151             if (context.charEquals(nextChar, 'Z'))
152             {
153                 context.setParsed(ZoneOffset.UTC);
154                 return position + 1;
155             }
156             return ~position;
157         }
158         context.setParsed(ZoneRegion.of(parsedZoneId));
159         return ppos.getIndex();
160     }
161 
162     /**
163      * Parse an offset following a prefix and set the ZoneId if it is valid.
164      * To matching the parsing of ZoneId.of the values are not normalized
165      * to ZoneOffsets.
166      *
167      * @param context the parse context
168      * @param text the input text
169      * @param prefixPos start of the prefix
170      * @param position start of text after the prefix
171      * @param parser parser for the value after the prefix
172      * @return the position after the parse
173      */
174     private int parseOffsetBased(DateTimeParseContext context, string text,
175             int prefixPos, int position, OffsetIdPrinterParser parser)
176     {
177         string prefix = toUpper(cast(string)(text[prefixPos .. position]));
178         if (position >= text.length)
179         {
180             context.setParsed(ZoneRegion.of(prefix));
181             return position;
182         }
183 
184         // '0' or 'Z' after prefix is not part of a valid ZoneId; use bare prefix
185         if (text[position] == '0' || context.charEquals(text[position], 'Z'))
186         {
187             context.setParsed(ZoneRegion.of(prefix));
188             return position;
189         }
190 
191         DateTimeParseContext newContext = context.copy();
192         int endPos = parser.parse(newContext, text, position);
193         try
194         {
195             if (endPos < 0)
196             {
197                 if (parser == OffsetIdPrinterParser.INSTANCE_ID_Z)
198                 {
199                     return ~prefixPos;
200                 }
201                 context.setParsed(ZoneRegion.of(prefix));
202                 return position;
203             }
204             int offset = cast(int) newContext.getParsed(ChronoField.OFFSET_SECONDS)
205                 .longValue();
206             ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(offset);
207             context.setParsed(ZoneRegion.ofOffset(prefix, zoneOffset));
208             return endPos;
209         }
210         catch (DateTimeException dte)
211         {
212             return ~prefixPos;
213         }
214     }
215 
216     override public string toString()
217     {
218         return description;
219     }
220 }