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