1 module text.xml.Convert; 2 3 version(unittest) import dshould; 4 import std.array; 5 static import std.conv; 6 import std.datetime; 7 import std.exception; 8 import std.string; 9 import std.traits; 10 static import text.time.Convert; 11 import text.xml.XmlException; 12 13 /** 14 * This service class provides static member functions to convert between values and their representations 15 * according to the data type definitions of the XML Schema language. When a conversion (from representation to value) 16 * cannot be performed, an exception indicating the XML validity violation is thrown. 17 * @Immutable 18 */ 19 class Convert 20 { 21 22 /** 23 * Converts the specified representation into its boolean value. 24 * 25 * Throws: XmlException on validity violation. 26 */ 27 public static T to(T : bool)(string value) 28 { 29 switch (value.strip) 30 { 31 case "false": 32 case "0": 33 return false; 34 case "true": 35 case "1": 36 return true; 37 default: 38 throw new XmlException(format!`"%s" is not a valid value of type boolean`(value)); 39 } 40 } 41 42 unittest 43 { 44 to!bool(" true ").should.equal(true); 45 to!bool(" false ").should.equal(false); 46 47 to!bool("True").should.throwAn!XmlException; 48 } 49 50 /** 51 * Converts the specified representation into its integer or floating-point value. 52 * 53 * Throws: XmlException on validity violation. 54 */ 55 public static T to(T)(string value) 56 if ((isIntegral!T && !is(T == enum)) || isFloatingPoint!T) 57 { 58 try 59 { 60 return std.conv.to!T(value.strip); 61 } 62 catch (std.conv.ConvException) 63 { 64 throw new XmlException(format!`"%s" is not a valid value of type %s`(value, T.stringof)); 65 } 66 } 67 68 unittest 69 { 70 to!int(" -1 ").should.equal(-1); 71 to!uint(" 0 ").should.equal(0); 72 to!long("-9223372036854775808").should.equal(long.min); 73 to!ulong(" 18446744073709551615 ").should.equal(ulong.max); 74 to!double(" 1.2 ").should.be.approximately(1.2, error = 1e-6); 75 76 to!int("1.2").should.throwAn!XmlException; 77 to!uint("0xab").should.throwAn!XmlException; 78 } 79 80 /** 81 * Converts the specified representation into its positive integer value. 82 * 83 * Throws: XmlException on validity violation. 84 */ 85 public static T toPositive(T)(string value) 86 if (isIntegral!T) 87 out (result; result > 0) 88 { 89 try 90 { 91 T result = std.conv.to!T(value.strip); 92 93 if (result <= 0) 94 { 95 throw new XmlException(format!`"%s" is not a valid value of type positive integer`(value)); 96 } 97 return result; 98 } 99 catch (std.conv.ConvException) 100 { 101 throw new XmlException(format!`"%s" is not a valid value of type %s`(value, T.stringof)); 102 } 103 } 104 105 unittest 106 { 107 toPositive!int(" +1 ").should.equal(1); 108 toPositive!long("9223372036854775807").should.equal(long.max); 109 110 toPositive!uint("0").should.throwAn!XmlException; 111 } 112 113 /** 114 * Converts the specified representation into its enumeration value. 115 * 116 * Throws: XmlException on validity violation. 117 */ 118 public static T to(T)(string value) 119 if (is(T == enum)) 120 { 121 try 122 { 123 return std.conv.to!T(value.strip); 124 } 125 catch (std.conv.ConvException) 126 { 127 throw new XmlException(format!`"%s" is not a valid value of enumeration %s`(value, T.stringof)); 128 } 129 } 130 131 unittest 132 { 133 enum Enum {VALUE} 134 135 to!Enum(" VALUE ").should.equal(Enum.VALUE); 136 137 to!Enum(" 0 ").should.throwAn!XmlException; 138 } 139 140 /** 141 * Converts the specified representation into its date value. 142 * 143 * Throws: XmlException on validity violation. 144 */ 145 public static T to(T : Date)(string value) 146 { 147 import std.ascii : isDigit; 148 149 try 150 { 151 return text.time.Convert.Convert.to!Date(value.strip); 152 } 153 catch (DateTimeException) 154 { 155 value = value.strip; 156 157 size_t index = value.length; 158 159 enum NoResult = ptrdiff_t.max; 160 161 ptrdiff_t endsWithTimezone() 162 { 163 import std.algorithm : max; 164 165 if (value.endsWith("Z")) 166 { 167 return "Z".length; 168 } 169 170 auto offsetIndex = max(value.lastIndexOf('+'), value.lastIndexOf('-')); 171 172 if (offsetIndex == -1) 173 { 174 return NoResult; 175 } 176 177 auto tzRange = value[offsetIndex + 1 .. $]; 178 179 if (tzRange.length != "00:00".length) 180 { 181 return NoResult; 182 } 183 184 if (isDigit(tzRange[0]) && isDigit(tzRange[1]) && tzRange[2] == ':' 185 && isDigit(tzRange[3]) && isDigit(tzRange[4])) 186 { 187 return "+00:00".length; 188 } 189 else 190 { 191 return NoResult; 192 } 193 } 194 195 auto timezoneLength = endsWithTimezone(); 196 197 if (timezoneLength != NoResult) 198 { 199 index -= timezoneLength; 200 } 201 try 202 { 203 return cast(Date) to!SysTime(value[0 .. index] ~ "T00:00:00" ~ value[index .. $]); 204 } 205 catch (XmlException) 206 { 207 throw new XmlException(format!`"%s" is not a valid value of type date`(value)); 208 } 209 } 210 } 211 212 unittest 213 { 214 to!Date("2003-02-01").should.equal(Date(2003, 2, 1)); 215 to!Date("2003-02-01Z").should.equal(Date(2003, 2, 1)); 216 to!Date("2003-02-01-01:00").should.equal(Date(2003, 2, 1)); 217 218 to!Date("01.02.2003").should.throwAn!XmlException; 219 to!Date("today").should.throwAn!XmlException; 220 } 221 222 /** 223 * Converts the specified representation into its date and time value. 224 * 225 * Throws: XmlException on validity violation. 226 */ 227 public static T to(T : SysTime)(string value) 228 { 229 try 230 { 231 return text.time.Convert.Convert.to!SysTime(value.strip); 232 } 233 catch (DateTimeException) 234 { 235 return fixDateTimeInTooDistantFuture(value); 236 } 237 } 238 239 unittest 240 { 241 to!SysTime(" 2003-02-01T11:55:00+01:00 ") 242 .should.equal(SysTime(DateTime(2003, 2, 1, 11, 55), new immutable SimpleTimeZone(1.hours))); 243 to!SysTime("292278994-08-17T08:12:55+01:00").should.equal(SysTime.max); 244 245 to!SysTime("2003-02-01 11:55:00").should.throwAn!XmlException 246 .because("missing 'T'"); 247 to!SysTime("2003-02-01T24:00:00").should.throwAn!XmlException 248 .because("XML Schema 1.1 not yet supported"); 249 } 250 251 /** 252 * Throws: XmlException when the given value does not match the lexical representation of a date and time, 253 * or when the date is not in the too distant future. 254 */ 255 private static SysTime fixDateTimeInTooDistantFuture(string value) @safe 256 { 257 // std.regex unduly explodes our compiletime 258 259 // import std.regex; 260 // auto lexicalRepresentation = regex(`^(?P<year>-?\d{4,})-(?P<month>\d{2})-(?P<day>\d{2})T` 261 // ~ `(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?P<timezone>(Z|[+-]\d{2}:\d{2})?)$`); 262 263 string yearStr; 264 bool matchLexicalRepresentationSaveYear() @nogc @safe 265 { 266 import std.ascii : isDigit; 267 import text.RecursiveDescentParser : RecursiveDescentParser; 268 269 with (RecursiveDescentParser(value)) 270 { 271 alias acceptDigits = i => matchTimes(i, () => acceptAsciiChar(ch => ch.isDigit)); 272 273 // ^(?P<year>-?\d{4,}) 274 if (!captureGroupInto(yearStr, () => 275 matchOptional(() => accept("-")) 276 && acceptDigits(4) && matchZeroOrMore(() => acceptDigits(1)))) 277 { 278 return false; 279 } 280 281 // -\d{2}-\d{2}T\d{2}:\d{2}:\d{2} 282 if (!(accept("-") 283 && acceptDigits(2) && accept("-") 284 && acceptDigits(2) && accept("T") 285 && acceptDigits(2) && accept(":") 286 && acceptDigits(2) && accept(":") 287 && acceptDigits(2))) 288 { 289 return false; 290 } 291 292 // (Z|[+-]\d{2}:\d{2})? 293 accept("Z") || matchGroup(() => 294 acceptAsciiChar((ch) => ch == '+' || ch == '-') 295 && acceptDigits(2) && accept(":") && acceptDigits(2)); 296 297 // $ 298 if (!eof) 299 { 300 return false; 301 } 302 303 return true; 304 } 305 } 306 307 // if (auto captures = value.strip.matchFirst(lexicalRepresentation)) 308 if (matchLexicalRepresentationSaveYear) 309 { 310 try 311 { 312 const year = std.conv.to!long(yearStr); 313 314 if (year > SysTime.max.year) 315 { 316 return SysTime.max; 317 } 318 } 319 catch (std.conv.ConvException) 320 { 321 // fall through 322 } 323 } 324 throw new XmlException(format!`"%s" is not a valid value of type date-time`(value)); 325 } 326 327 /** 328 * Converts the specified representation into its duration (strictly speaking, 'dayTimeDuration') value. 329 * For decimal fractions, digits representing less than one millisecond are disregarded. 330 * 331 * Throws: XmlException on validity violation. 332 */ 333 public static T to(T : Duration)(string value) 334 { 335 try 336 { 337 return text.time.Convert.Convert.to!Duration(value.strip); 338 } 339 catch (TimeException) 340 { 341 throw new XmlException(format!`"%s" is not a valid value of type duration`(value)); 342 } 343 } 344 345 unittest 346 { 347 to!Duration("P1DT2H3M4.5S").should.equal(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs); 348 349 to!Duration("PT1S2M3H").should.throwAn!XmlException. 350 because("disarranged representation"); 351 } 352 353 /** 354 * Converts the specified representation into its time value. 355 * 356 * Throws: XmlException on validity violation. 357 */ 358 public static T to(T : TimeOfDay)(string value) 359 { 360 try 361 { 362 return text.time.Convert.Convert.to!TimeOfDay(value.strip); 363 } 364 catch (DateTimeException) 365 { 366 throw new XmlException(format!`"%s" is not a valid value of type time`(value)); 367 } 368 } 369 370 unittest 371 { 372 Convert.to!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3)); 373 374 to!TimeOfDay("1:2:3").should.throwAn!XmlException; 375 to!TimeOfDay("24:00:00").should.throwAn!XmlException 376 .because("XML Schema 1.1 not yet supported"); 377 } 378 379 /** 380 * Returns the specified string value. 381 * This specialization allows to use the template with any relevant type. 382 */ 383 public static T to(T : string)(string value) 384 { 385 return value; 386 } 387 388 /** 389 * Converts the specified "time" representation (time of day with optional fractional seconds) 390 * into the corresponding time of day (without fractional seconds). 391 * 392 * Throws: XmlException on validity violation. 393 */ 394 public static T toTime(T : TimeOfDay)(string value) 395 { 396 return TimeOfDay.min + toTime!Duration(value); 397 } 398 399 @safe unittest 400 { 401 toTime!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3)); 402 toTime!TimeOfDay("01:02:03.456").should.equal(TimeOfDay(1, 2, 3)); 403 } 404 405 /** 406 * Converts the specified "time" representation (time of day with optional fractional seconds) 407 * into the corresponding duration since midnight. 408 * 409 * Throws: XmlException on validity violation. 410 */ 411 public static T toTime(T : Duration = Duration)(string value) 412 { 413 import std.algorithm : findSplitBefore; 414 415 auto result = value.strip.findSplitBefore("."); 416 417 try 418 { 419 const timeOfDay = TimeOfDay.fromISOExtString(result[0]); 420 const fracSecs = fracSecsFromISOString(result[1]); 421 422 return timeOfDay - TimeOfDay.min + fracSecs; 423 } 424 catch (DateTimeException) 425 { 426 throw new XmlException(format!`"%s" is not a valid value of type time`(value)); 427 } 428 } 429 430 @safe unittest 431 { 432 toTime("01:02:03").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min); 433 toTime("01:02:03.456").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min + 456.msecs); 434 } 435 436 /** 437 * See_Also: private helper function of std.datetime with same name 438 * Throws: DateTimeException on syntax error. 439 */ 440 private static Duration fracSecsFromISOString(string value) @safe 441 { 442 import std.conv : ConvException, to; 443 import std.range : empty; 444 445 if (value.empty) 446 { 447 return Duration.zero; 448 } 449 450 enforce!DateTimeException(value.front == '.' && value.length > 1); 451 452 char[7] digits; // hnsecs 453 454 foreach (i, ref digit; digits) 455 { 456 digit = (i + 1 < value.length) ? value[i + 1] : '0'; 457 } 458 try 459 { 460 return digits.to!int.hnsecs; 461 } 462 catch (ConvException exception) 463 { 464 throw new DateTimeException(exception.msg); 465 } 466 467 } 468 469 unittest 470 { 471 fracSecsFromISOString("").should.equal(Duration.zero); 472 fracSecsFromISOString(".1").should.equal(1_000_000.hnsecs); 473 fracSecsFromISOString(".01").should.equal(100_000.hnsecs); 474 fracSecsFromISOString(".001").should.equal(10_000.hnsecs); 475 fracSecsFromISOString(".0001").should.equal(1_000.hnsecs); 476 fracSecsFromISOString(".00001").should.equal(100.hnsecs); 477 fracSecsFromISOString(".000001").should.equal(10.hnsecs); 478 fracSecsFromISOString(".0000001").should.equal(1.hnsecs); 479 480 fracSecsFromISOString("?").should.throwA!DateTimeException; 481 fracSecsFromISOString(".").should.throwA!DateTimeException; 482 fracSecsFromISOString("...").should.throwA!DateTimeException; 483 } 484 485 /** 486 * Converts the specified boolean value into its canonical representation. 487 */ 488 public static string toString(bool value) @nogc @safe 489 { 490 return value ? "true" : "false"; 491 } 492 493 @safe unittest 494 { 495 toString(true).should.equal("true"); 496 toString(false).should.equal("false"); 497 } 498 499 /** 500 * Converts the specified integer or floating-point value into its canonical representation. 501 */ 502 public static string toString(T)(T value) 503 if (isIntegral!T || isFloatingPoint!T) 504 { 505 return std.conv.to!string(value); 506 } 507 508 @safe unittest 509 { 510 toString(42).should.equal("42"); 511 toString(-42).should.equal("-42"); 512 toString(1.2).should.equal("1.2"); 513 } 514 515 /** 516 * Converts the specified date into its canonical representation. 517 */ 518 public static string toString(Date date) @safe 519 { 520 return text.time.Convert.Convert.toString(date); 521 } 522 523 @safe unittest 524 { 525 toString(Date(2003, 2, 1)).should.equal("2003-02-01"); 526 } 527 528 /** 529 * Converts the specified date and time value into its canonical representation. 530 */ 531 public static string toString(SysTime dateTime) @safe 532 { 533 return text.time.Convert.Convert.toString(dateTime); 534 } 535 536 @safe unittest 537 { 538 DateTime dateTime = DateTime.fromISOExtString("2003-02-01T11:55:00"); 539 540 toString(SysTime(dateTime)).should.equal("2003-02-01T11:55:00"); 541 toString(SysTime(dateTime, UTC())).should.equal("2003-02-01T11:55:00Z"); 542 } 543 544 /** 545 * Converts the specified duration value into its canonical representation. 546 */ 547 public static string toString(Duration duration) @safe 548 { 549 return text.time.Convert.Convert.toString(duration); 550 } 551 552 @safe unittest 553 { 554 toString(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs).should.equal("P1DT2H3M4.5S"); 555 } 556 557 /** 558 * Converts the specified time of day value into its canonical representation. 559 */ 560 public static string toString(TimeOfDay timeOfDay) @safe 561 { 562 return text.time.Convert.Convert.toString(timeOfDay); 563 } 564 565 @safe unittest 566 { 567 toString(TimeOfDay(1, 2, 3)).should.equal("01:02:03"); 568 } 569 570 }