1 /** 2 * Time formatting functions. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.time.format; 15 16 import std.algorithm.comparison; 17 import std.conv : text; 18 import std.datetime; 19 import std.format; 20 import std.math : abs; 21 22 import ae.utils.meta; 23 import ae.utils.text; 24 import ae.utils.textout; 25 import ae.utils.time.common; 26 import ae.utils.time.types : AbsTime; 27 28 private struct FormatContext 29 { 30 SysTime t; 31 DateTime dt; 32 bool escaping; 33 } 34 35 private FormatContext makeContext(SysTime t) { return FormatContext(t, cast(DateTime)t); } 36 private FormatContext makeContext(DateTime t) { return FormatContext(SysTime(t), t); } 37 private FormatContext makeContext(Date t) { return FormatContext(SysTime(t), DateTime(t)); } 38 private FormatContext makeContext(AbsTime t) { auto s = t.sysTime(UTC()); return FormatContext(s, cast(DateTime)s); } 39 // TODO: TimeOfDay support 40 41 private void putToken(alias c, alias context, alias sink)() 42 { 43 with (context) 44 { 45 void putOneDigit(uint i) 46 { 47 debug assert(i < 10); 48 sink.put(cast(char)('0' + i)); 49 } 50 51 void putOneOrTwoDigits(uint i) 52 { 53 debug assert(i < 100); 54 if (i >= 10) 55 { 56 sink.put(cast(char)('0' + (i / 10))); 57 sink.put(cast(char)('0' + (i % 10))); 58 } 59 else 60 sink.put(cast(char)('0' + i )); 61 } 62 63 void putTimezoneName(string tzStr) 64 { 65 if (tzStr.length) 66 sink.put(tzStr[0..min($, MaxTimezoneNameLength)]); 67 else 68 // if (t.timezone.utcToTZ(t.stdTime) == t.stdTime) 69 // sink.put("UTC"); 70 // else 71 { 72 enum fmt = TimeFormatElement.timezoneOffsetWithColon; 73 putToken!(fmt, context, sink)(); 74 } 75 } 76 77 if (escaping) 78 sink.put(c), escaping = false; 79 else 80 switch (c) 81 { 82 // Day 83 case TimeFormatElement.dayOfMonthZeroPadded: 84 sink.put(toDecFixed!2(dt.day)); 85 break; 86 case TimeFormatElement.dayOfWeekNameShort: 87 sink.put(WeekdayShortNames[dt.dayOfWeek]); 88 break; 89 case TimeFormatElement.dayOfMonth: 90 putOneOrTwoDigits(dt.day); 91 break; 92 case TimeFormatElement.dayOfWeekName: 93 sink.put(WeekdayLongNames[dt.dayOfWeek]); 94 break; 95 case TimeFormatElement.dayOfWeekIndexISO8601: 96 putOneDigit((dt.dayOfWeek+6)%7 + 1); 97 break; 98 case TimeFormatElement.dayOfMonthOrdinalSuffix: 99 switch (dt.day) 100 { 101 case 1: 102 case 21: 103 case 31: 104 sink.put("st"); 105 break; 106 case 2: 107 case 22: 108 sink.put("nd"); 109 break; 110 case 3: 111 case 23: 112 sink.put("rd"); 113 break; 114 default: 115 sink.put("th"); 116 } 117 break; 118 case TimeFormatElement.dayOfWeekIndex: 119 putOneDigit(cast(int)dt.dayOfWeek); 120 break; 121 case TimeFormatElement.dayOfYear: 122 sink.put(text(dt.dayOfYear-1)); 123 break; 124 125 // Week 126 case TimeFormatElement.weekOfYear: 127 sink.put(toDecFixed!2(dt.isoWeek)); 128 break; 129 130 // Month 131 case TimeFormatElement.monthName: 132 sink.put(MonthLongNames[dt.month-1]); 133 break; 134 case TimeFormatElement.monthZeroPadded: 135 sink.put(toDecFixed!2(dt.month)); 136 break; 137 case TimeFormatElement.monthNameShort: 138 sink.put(MonthShortNames[dt.month-1]); 139 break; 140 case TimeFormatElement.month: 141 putOneOrTwoDigits(dt.month); 142 break; 143 case TimeFormatElement.daysInMonth: 144 putOneOrTwoDigits(dt.daysInMonth); 145 break; 146 147 // Year 148 case TimeFormatElement.yearIsLeapYear: 149 sink.put(dt.isLeapYear ? '1' : '0'); 150 break; 151 // case TimeFormatElement.yearForWeekNumbering: TODO (ISO 8601 year number) 152 case TimeFormatElement.year: 153 sink.put(toDecFixed!4(cast(uint)dt.year)); // Hack? Assumes years are in 1000-9999 AD range 154 break; 155 case TimeFormatElement.yearOfCentury: 156 sink.put(toDecFixed!2(cast(uint)dt.year % 100)); 157 break; 158 159 // Time 160 case TimeFormatElement.ampmLower: 161 sink.put(dt.hour < 12 ? "am" : "pm"); 162 break; 163 case TimeFormatElement.ampmUpper: 164 sink.put(dt.hour < 12 ? "AM" : "PM"); 165 break; 166 // case TimeFormatElement.swatchInternetTime: TODO (Swatch Internet time) 167 case TimeFormatElement.hour12: 168 putOneOrTwoDigits((dt.hour+11)%12 + 1); 169 break; 170 case TimeFormatElement.hour: 171 putOneOrTwoDigits(dt.hour); 172 break; 173 case TimeFormatElement.hour12ZeroPadded: 174 sink.put(toDecFixed!2(cast(uint)(dt.hour+11)%12 + 1)); 175 break; 176 case TimeFormatElement.hourZeroPadded: 177 sink.put(toDecFixed!2(dt.hour)); 178 break; 179 case TimeFormatElement.minute: 180 sink.put(toDecFixed!2(dt.minute)); 181 break; 182 case TimeFormatElement.second: 183 sink.put(toDecFixed!2(dt.second)); 184 break; 185 case TimeFormatElement.microseconds: 186 sink.put(toDecFixed!6(cast(uint)t.fracSecs.split!"usecs".usecs)); 187 break; 188 case TimeFormatElement.milliseconds: 189 case TimeFormatElement.millisecondsAlt: // not standard 190 sink.put(toDecFixed!3(cast(uint)t.fracSecs.split!"msecs".msecs)); 191 break; 192 case TimeFormatElement.nanoseconds: // not standard 193 sink.put(toDecFixed!9(cast(uint)t.fracSecs.split!"nsecs".nsecs)); 194 break; 195 196 // Timezone 197 case TimeFormatElement.timezoneName: 198 putTimezoneName(t.timezone.name); 199 break; 200 case TimeFormatElement.isDST: 201 sink.put(t.dstInEffect ? '1': '0'); 202 break; 203 case TimeFormatElement.timezoneOffsetWithoutColon: 204 { 205 auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60; 206 sink.reference.formattedWrite("%+03d%02d", minutes/60, abs(minutes%60)); 207 break; 208 } 209 case TimeFormatElement.timezoneOffsetWithColon: 210 { 211 auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60; 212 sink.reference.formattedWrite("%+03d:%02d", minutes/60, abs(minutes%60)); 213 break; 214 } 215 case TimeFormatElement.timezoneAbbreviation: 216 putTimezoneName(t.timezone.stdName); 217 break; 218 case TimeFormatElement.timezoneOffsetSeconds: 219 sink.putDecimal((t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000); 220 break; 221 222 // Full date/time 223 case TimeFormatElement.dateTimeISO8601: 224 sink.put(dt.toISOExtString()); 225 break; 226 case TimeFormatElement.dateTimeRFC2822: 227 putTime(sink, t, TimeFormats.RFC2822); 228 break; 229 case TimeFormatElement.dateTimeUNIX: 230 sink.putDecimal(t.toUnixTime()); 231 break; 232 233 // Escape next character 234 case TimeFormatElement.escapeNextCharacter: 235 escaping = true; 236 break; 237 238 // Other characters (whitespace, delimiters) 239 default: 240 put(sink, c); 241 } 242 } 243 } 244 245 enum isFormattableTime(T) = is(typeof({ T t = void; return makeContext(t); })); 246 247 /// Format a time value using the format spec fmt. 248 /// This version generates specialized code for the given fmt. 249 string formatTime(string fmt, Time)(Time t) 250 if (isFormattableTime!Time) 251 { 252 enum maxSize = timeFormatSize(fmt); 253 auto result = StringBuilder(maxSize); 254 putTime!fmt(result, t); 255 return result.get(); 256 } 257 258 /// ditto 259 void putTime(string fmt, S, Time)(ref S sink, Time t) 260 if (isStringSink!S && isFormattableTime!Time) 261 { 262 putTimeImpl!fmt(sink, t); 263 } 264 265 /// Format a time value using the format spec fmt. 266 /// This version parses fmt at runtime. 267 string formatTime(Time)(Time t, string fmt) 268 if (isFormattableTime!Time) 269 { 270 auto result = StringBuilder(timeFormatSize(fmt)); 271 putTime(result, t, fmt); 272 return result.get(); 273 } 274 275 /// ditto 276 deprecated string formatTime(string fmt, SysTime t = Clock.currTime()) 277 { 278 auto result = StringBuilder(48); 279 putTime(result, fmt, t); 280 return result.get(); 281 } 282 283 /// ditto 284 void putTime(S, Time)(ref S sink, Time t, string fmt) 285 if (isStringSink!S && isFormattableTime!Time) 286 { 287 putTimeImpl!fmt(sink, t); 288 } 289 290 /// ditto 291 deprecated void putTime(S)(ref S sink, string fmt, SysTime t = Clock.currTime()) 292 if (isStringSink!S) 293 { 294 putTimeImpl!fmt(sink, t); 295 } 296 297 private void putTimeImpl(alias fmt, S, Time)(ref S sink, Time t) 298 if (isFormattableTime!Time) 299 { 300 auto context = makeContext(t); 301 foreach (c; CTIterate!fmt) 302 putToken!(c, context, sink)(); 303 } 304 305 unittest 306 { 307 assert(SysTime.fromUnixTime(0, UTC()).formatTime!(TimeFormats.STD_DATE) == "Thu Jan 01 00:00:00 GMT+0000 1970"); 308 assert(SysTime(0, new immutable(SimpleTimeZone)(Duration.zero)).formatTime!"T" == "+00:00"); 309 310 assert((cast(DateTime)SysTime.fromUnixTime(0, UTC())).formatTime!(TimeFormats.HTML5DATE) == "1970-01-01"); 311 312 assert(AbsTime(1).formatTime!(TimeFormats.HTML5DATE) == "0001-01-01"); 313 }