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 * Converts the specified representation to an arbitrary type, 253 * so long the type has a fromString method. 254 */ 255 public static T to(T)(string value) 256 if (__traits(compiles, T.fromString(value))) 257 { 258 return T.fromString(value); 259 } 260 261 @("Convert.to with fromString") 262 unittest 263 { 264 struct S 265 { 266 int i; 267 268 static S fromString(string s) 269 { 270 import std.conv : to; 271 272 return S(s.to!int); 273 } 274 } 275 276 Convert.to!S("5").should.equal(S(5)); 277 } 278 279 /** 280 * Throws: XmlException when the given value does not match the lexical representation of a date and time, 281 * or when the date is not in the too distant future. 282 */ 283 private static SysTime fixDateTimeInTooDistantFuture(string value) @safe 284 { 285 // std.regex unduly explodes our compiletime 286 287 // import std.regex; 288 // auto lexicalRepresentation = regex(`^(?P<year>-?\d{4,})-(?P<month>\d{2})-(?P<day>\d{2})T` 289 // ~ `(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?P<timezone>(Z|[+-]\d{2}:\d{2})?)$`); 290 291 string yearStr; 292 bool matchLexicalRepresentationSaveYear() @nogc @safe 293 { 294 import std.ascii : isDigit; 295 import text.RecursiveDescentParser : RecursiveDescentParser; 296 297 with (RecursiveDescentParser(value)) 298 { 299 alias acceptDigits = i => matchTimes(i, () => acceptAsciiChar(ch => ch.isDigit)); 300 301 // ^(?P<year>-?\d{4,}) 302 if (!captureGroupInto(yearStr, () => 303 matchOptional(() => accept("-")) 304 && acceptDigits(4) && matchZeroOrMore(() => acceptDigits(1)))) 305 { 306 return false; 307 } 308 309 // -\d{2}-\d{2}T\d{2}:\d{2}:\d{2} 310 if (!(accept("-") 311 && acceptDigits(2) && accept("-") 312 && acceptDigits(2) && accept("T") 313 && acceptDigits(2) && accept(":") 314 && acceptDigits(2) && accept(":") 315 && acceptDigits(2))) 316 { 317 return false; 318 } 319 320 // (Z|[+-]\d{2}:\d{2})? 321 accept("Z") || matchGroup(() => 322 acceptAsciiChar((ch) => ch == '+' || ch == '-') 323 && acceptDigits(2) && accept(":") && acceptDigits(2)); 324 325 // $ 326 if (!eof) 327 { 328 return false; 329 } 330 331 return true; 332 } 333 } 334 335 // if (auto captures = value.strip.matchFirst(lexicalRepresentation)) 336 if (matchLexicalRepresentationSaveYear) 337 { 338 try 339 { 340 const year = std.conv.to!long(yearStr); 341 342 if (year > SysTime.max.year) 343 { 344 return SysTime.max; 345 } 346 } 347 catch (std.conv.ConvException) 348 { 349 // fall through 350 } 351 } 352 throw new XmlException(format!`"%s" is not a valid value of type date-time`(value)); 353 } 354 355 /** 356 * Converts the specified representation into its duration (strictly speaking, 'dayTimeDuration') value. 357 * For decimal fractions, digits representing less than one millisecond are disregarded. 358 * 359 * Throws: XmlException on validity violation. 360 */ 361 public static T to(T : Duration)(string value) 362 { 363 try 364 { 365 return text.time.Convert.Convert.to!Duration(value.strip); 366 } 367 catch (TimeException) 368 { 369 throw new XmlException(format!`"%s" is not a valid value of type duration`(value)); 370 } 371 } 372 373 unittest 374 { 375 to!Duration("P1DT2H3M4.5S").should.equal(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs); 376 377 to!Duration("PT1S2M3H").should.throwAn!XmlException. 378 because("disarranged representation"); 379 } 380 381 /** 382 * Converts the specified representation into its time value. 383 * 384 * Throws: XmlException on validity violation. 385 */ 386 public static T to(T : TimeOfDay)(string value) 387 { 388 try 389 { 390 return text.time.Convert.Convert.to!TimeOfDay(value.strip); 391 } 392 catch (DateTimeException) 393 { 394 throw new XmlException(format!`"%s" is not a valid value of type time`(value)); 395 } 396 } 397 398 unittest 399 { 400 Convert.to!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3)); 401 402 to!TimeOfDay("1:2:3").should.throwAn!XmlException; 403 to!TimeOfDay("24:00:00").should.throwAn!XmlException 404 .because("XML Schema 1.1 not yet supported"); 405 } 406 407 /** 408 * Returns the specified string value. 409 * This specialization allows to use the template with any relevant type. 410 */ 411 public static T to(T : string)(string value) 412 { 413 return value; 414 } 415 416 /** 417 * Converts the specified "time" representation (time of day with optional fractional seconds) 418 * into the corresponding time of day (without fractional seconds). 419 * 420 * Throws: XmlException on validity violation. 421 */ 422 public static T toTime(T : TimeOfDay)(string value) 423 { 424 return TimeOfDay.min + toTime!Duration(value); 425 } 426 427 @safe unittest 428 { 429 toTime!TimeOfDay("01:02:03").should.equal(TimeOfDay(1, 2, 3)); 430 toTime!TimeOfDay("01:02:03.456").should.equal(TimeOfDay(1, 2, 3)); 431 } 432 433 /** 434 * Converts the specified "time" representation (time of day with optional fractional seconds) 435 * into the corresponding duration since midnight. 436 * 437 * Throws: XmlException on validity violation. 438 */ 439 public static T toTime(T : Duration = Duration)(string value) 440 { 441 import std.algorithm : findSplitBefore; 442 443 auto result = value.strip.findSplitBefore("."); 444 445 try 446 { 447 const timeOfDay = TimeOfDay.fromISOExtString(result[0]); 448 const fracSecs = fracSecsFromISOString(result[1]); 449 450 return timeOfDay - TimeOfDay.min + fracSecs; 451 } 452 catch (DateTimeException) 453 { 454 throw new XmlException(format!`"%s" is not a valid value of type time`(value)); 455 } 456 } 457 458 @safe unittest 459 { 460 toTime("01:02:03").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min); 461 toTime("01:02:03.456").should.equal(TimeOfDay(1, 2, 3) - TimeOfDay.min + 456.msecs); 462 } 463 464 /** 465 * See_Also: private helper function of std.datetime with same name 466 * Throws: DateTimeException on syntax error. 467 */ 468 private static Duration fracSecsFromISOString(string value) @safe 469 { 470 import std.conv : ConvException, to; 471 import std.range : empty; 472 473 if (value.empty) 474 { 475 return Duration.zero; 476 } 477 478 enforce!DateTimeException(value.front == '.' && value.length > 1); 479 480 char[7] digits; // hnsecs 481 482 foreach (i, ref digit; digits) 483 { 484 digit = (i + 1 < value.length) ? value[i + 1] : '0'; 485 } 486 try 487 { 488 return digits.to!int.hnsecs; 489 } 490 catch (ConvException exception) 491 { 492 throw new DateTimeException(exception.msg); 493 } 494 495 } 496 497 unittest 498 { 499 fracSecsFromISOString("").should.equal(Duration.zero); 500 fracSecsFromISOString(".1").should.equal(1_000_000.hnsecs); 501 fracSecsFromISOString(".01").should.equal(100_000.hnsecs); 502 fracSecsFromISOString(".001").should.equal(10_000.hnsecs); 503 fracSecsFromISOString(".0001").should.equal(1_000.hnsecs); 504 fracSecsFromISOString(".00001").should.equal(100.hnsecs); 505 fracSecsFromISOString(".000001").should.equal(10.hnsecs); 506 fracSecsFromISOString(".0000001").should.equal(1.hnsecs); 507 508 fracSecsFromISOString("?").should.throwA!DateTimeException; 509 fracSecsFromISOString(".").should.throwA!DateTimeException; 510 fracSecsFromISOString("...").should.throwA!DateTimeException; 511 } 512 513 /** 514 * Converts the specified boolean value into its canonical representation. 515 */ 516 public static string toString(bool value) @nogc @safe 517 { 518 return value ? "true" : "false"; 519 } 520 521 @safe unittest 522 { 523 toString(true).should.equal("true"); 524 toString(false).should.equal("false"); 525 } 526 527 /** 528 * Converts the specified integer or floating-point value into its canonical representation. 529 */ 530 public static string toString(T)(T value) 531 if (isIntegral!T || isFloatingPoint!T) 532 { 533 return std.conv.to!string(value); 534 } 535 536 @safe unittest 537 { 538 toString(42).should.equal("42"); 539 toString(-42).should.equal("-42"); 540 toString(1.2).should.equal("1.2"); 541 } 542 543 /** 544 * Converts the specified date into its canonical representation. 545 */ 546 public static string toString(Date date) @safe 547 { 548 return text.time.Convert.Convert.toString(date); 549 } 550 551 @safe unittest 552 { 553 toString(Date(2003, 2, 1)).should.equal("2003-02-01"); 554 } 555 556 /** 557 * Converts the specified date and time value into its canonical representation. 558 */ 559 public static string toString(SysTime dateTime) @safe 560 { 561 return text.time.Convert.Convert.toString(dateTime); 562 } 563 564 @safe unittest 565 { 566 DateTime dateTime = DateTime.fromISOExtString("2003-02-01T11:55:00"); 567 568 toString(SysTime(dateTime)).should.equal("2003-02-01T11:55:00"); 569 toString(SysTime(dateTime, UTC())).should.equal("2003-02-01T11:55:00Z"); 570 } 571 572 /** 573 * Converts the specified duration value into its canonical representation. 574 */ 575 public static string toString(Duration duration) @safe 576 { 577 return text.time.Convert.Convert.toString(duration); 578 } 579 580 @safe unittest 581 { 582 toString(1.days + 2.hours + 3.minutes + 4.seconds + 500.msecs).should.equal("P1DT2H3M4.5S"); 583 } 584 585 /** 586 * Converts the specified time of day value into its canonical representation. 587 */ 588 public static string toString(TimeOfDay timeOfDay) @safe 589 { 590 return text.time.Convert.Convert.toString(timeOfDay); 591 } 592 593 @safe unittest 594 { 595 toString(TimeOfDay(1, 2, 3)).should.equal("01:02:03"); 596 } 597 598 }