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 }