github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/timeutil/pgdate/field_extract.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package pgdate 12 13 import ( 14 "fmt" 15 "strconv" 16 "strings" 17 "time" 18 "unicode" 19 "unicode/utf8" 20 21 "github.com/cockroachdb/errors" 22 ) 23 24 // numberChunk associates a value with a leading separator, 25 // such as '-' or '/'. 26 type numberChunk struct { 27 separator rune 28 // The numeric value of the chunk. 29 v int 30 // The magnitude of the value (i.e. how many digits). 31 magnitude int 32 } 33 34 func (n numberChunk) String() string { 35 if n.separator == utf8.RuneError { 36 return fmt.Sprintf("%d", n.v) 37 } 38 return fmt.Sprintf("%v%d", n.separator, n.v) 39 } 40 41 // fieldExtract manages the state of a date/time parsing operation. 42 // This handles cases where a field, such as Julian date, 43 // would conflict with also setting the year. 44 type fieldExtract struct { 45 // The field data is stored in a fixed-size array. 46 data [fieldMaximum + 1]int 47 // Tracks the fields that have been set, to distinguish 0 from unset. 48 has fieldSet 49 // Provides a time for evaluating relative dates as well as a 50 // timezone. 51 now time.Time 52 mode ParseMode 53 // The fields that must be present to succeed. 54 required fieldSet 55 // Stores a reference to one of the sentinel values, to be returned 56 // by the makeDateTime() functions 57 sentinel *time.Time 58 // This indicates that the value in the year field was only 59 // two digits and should be adjusted to make it recent. 60 tweakYear bool 61 // Tracks the sign of the timezone offset. We need to track 62 // this separately from the sign of the tz1 value in case 63 // we're trying to store a (nonsensical) value like -0030. 64 tzSign int 65 // Tracks the fields that we want to extract. 66 wanted fieldSet 67 } 68 69 // Extract is the top-level function. It attempts to break the input 70 // string into a collection of date/time fields in order to populate a 71 // fieldExtract. 72 func (fe *fieldExtract) Extract(s string) error { 73 // Break the string into alphanumeric chunks. 74 textChunks := make([]stringChunk, fieldMaximum) 75 count, _ := chunk(s, textChunks) 76 77 if count < 0 { 78 return inputErrorf("too many fields in input") 79 } else if count == 0 { 80 return inputErrorf("empty or blank input") 81 } 82 83 // Create a place to store extracted numeric info. 84 numbers := make([]numberChunk, 0, fieldMaximum) 85 86 appendNumber := func(prefix, number string) error { 87 v, err := strconv.Atoi(number) 88 if err != nil { 89 return err 90 } 91 92 // Allow exactly one non-whitespace separator. 93 s := ' ' 94 for _, r := range prefix { 95 switch { 96 case s == ' ': 97 s = r 98 case unicode.IsSpace(r): 99 // Ignore whitespace characters. 100 default: 101 return inputErrorf(`detected multiple separators in "%s""`, prefix) 102 } 103 } 104 105 numbers = append(numbers, numberChunk{separator: s, v: v, magnitude: len(number)}) 106 return nil 107 } 108 109 var leftoverText string 110 111 // First, we'll try to pluck out any keywords that exist in the input. 112 // If a chunk is not a keyword or other special-case pattern, it 113 // must be a numeric value, which we'll pluck out for a second 114 // pass. If we see certain sentinel values, we'll pick them out, 115 // but keep going to ensure that the user hasn't written something 116 // like "epoch infinity". 117 for idx, chunk := range textChunks[:count] { 118 match := strings.ToLower(chunk.Match) 119 120 switch match { 121 case keywordEpoch: 122 if err := fe.matchedSentinel(TimeEpoch, match); err != nil { 123 return err 124 } 125 126 case keywordInfinity: 127 if strings.HasSuffix(chunk.NotMatch, "-") { 128 if err := fe.matchedSentinel(TimeNegativeInfinity, match); err != nil { 129 return err 130 } 131 } else { 132 if err := fe.matchedSentinel(TimeInfinity, match); err != nil { 133 return err 134 } 135 } 136 137 case keywordNow: 138 if err := fe.matchedSentinel(fe.now, match); err != nil { 139 return err 140 } 141 142 default: 143 // The most probable case is that we have a numeric input. 144 if err := appendNumber(chunk.NotMatch, match); err == nil { 145 continue 146 } 147 148 // Fan out to other keyword-based extracts. 149 if m, ok := keywordSetters[match]; ok { 150 if err := m(fe, match); err != nil { 151 return err 152 } 153 154 // This detects a format like 01-02-Jan. While we could 155 // figure it out if one of those were a four-digit number, 156 // this is consistent with PostgreSQL 10.5 behavior. 157 // We should only ever see a text month in field 0 or 1. 158 if idx == 2 && fe.Wants(fieldYear) && !fe.Wants(fieldMonth) && fe.Wants(fieldDay) { 159 return inputErrorf("no such thing as a YDM or DYM format") 160 } 161 continue 162 } 163 164 // Handle the oddball Z and Zulu suffixes. Try stripping the 165 // suffix and appending the resulting number. 166 if strings.HasSuffix(match, keywordZ) { 167 if err := fieldSetterUTC(fe, ""); err != nil { 168 return err 169 } 170 maybeMatch := match[:len(match)-len(keywordZ)] 171 if err := appendNumber(chunk.NotMatch, maybeMatch); err == nil { 172 continue 173 } 174 175 } else if strings.HasSuffix(match, keywordZulu) { 176 if err := fieldSetterUTC(fe, ""); err != nil { 177 return err 178 } 179 maybeMatch := match[:len(match)-len(keywordZulu)] 180 if err := appendNumber(chunk.NotMatch, maybeMatch); err == nil { 181 continue 182 } 183 } 184 185 // Try to parse Julian dates. 186 if matched, err := fieldSetterJulianDate(fe, match); matched { 187 if err != nil { 188 return err 189 } 190 continue 191 } 192 193 // Save off any leftover text, it might be a timezone name. 194 // We do this concatenation here because Region/City_Name 195 // would get split into two chunks. 196 leftoverText += strings.TrimSpace(chunk.NotMatch) + chunk.Match 197 } 198 } 199 200 // See if our leftover text is a timezone name. 201 if leftoverText != "" { 202 if loc, err := zoneCacheInstance.LoadLocation(leftoverText); err == nil { 203 // Save off the timezone for later resolution to an offset. 204 fe.now = fe.now.In(loc) 205 206 // Since we're using a named location, we must have a date 207 // in order to compute daylight-savings time. 208 fe.required = fe.required.AddAll(dateRequiredFields) 209 210 // Remove TZ fields from the wanted list, but add a date 211 // in order to resolve the location's DST. Also, if we had a 212 // text month, ensure that it's also not in the wanted field. 213 fe.wanted = fe.wanted.AddAll(dateFields).ClearAll(fe.has.Add(fieldTZHour).Add(fieldTZMinute).Add(fieldTZSecond)) 214 } else { 215 return inputErrorf(`could not parse "%s"`, s) 216 } 217 } 218 219 if fe.sentinel != nil { 220 return nil 221 } 222 223 // In the second pass, we'll use pattern-matching and the knowledge 224 // of which fields have already been set in order to keep picking 225 // out field data. 226 textMonth := !fe.Wants(fieldMonth) 227 for i := range numbers { 228 if fe.wanted == 0 { 229 return inputErrorf("too many input fields") 230 } 231 if err := fe.interpretNumber(numbers, i, textMonth); err != nil { 232 return err 233 } 234 } 235 236 return fe.validate() 237 } 238 239 // Get returns the value of the requested field and whether or not 240 // that field has indeed been set. 241 func (fe *fieldExtract) Get(field field) (int, bool) { 242 return fe.data[field], fe.has.Has(field) 243 } 244 245 // interpretNumber applies pattern-matching rules to figure out which 246 // field the next chunk of input should be applied to. 247 func (fe *fieldExtract) interpretNumber(numbers []numberChunk, idx int, textMonth bool) error { 248 chunk := numbers[idx] 249 switch { 250 case chunk.separator == '.': 251 // Example: 04:04:04.913231+00:00, a fractional second. 252 // ^^^^^^ 253 // Example: 1999.123, a year + day-of-year. 254 // ^^^ 255 switch { 256 case chunk.magnitude == 3 && 257 !fe.Wants(fieldYear) && fe.Wants(fieldMonth) && fe.Wants(fieldDay) && 258 chunk.v >= 1 && chunk.v <= 366: 259 // Example: 1999 001, a year and day-of-year. 260 // ^^^ 261 // Example: 1999 123 262 // ^^^ 263 // BUT NOT: 1999 1 264 return fe.SetDayOfYear(chunk) 265 266 case !fe.Wants(fieldSecond) && fe.Wants(fieldNanos): 267 // The only other place a period is valid is in a fractional 268 // second. We check to make sure that a second has been set. 269 270 // We need to "right-pad" the parsed integer value to nine 271 // places to wind up with a nanosecond value. Values with 272 // sub-nanosecond precision will be truncated. 273 var mult int 274 switch chunk.magnitude { 275 case 1: 276 mult = 100000000 277 case 2: 278 mult = 10000000 279 case 3: 280 mult = 1000000 281 case 4: 282 mult = 100000 283 case 5: 284 mult = 10000 285 case 6: 286 mult = 1000 287 case 7: 288 mult = 100 289 case 8: 290 mult = 10 291 case 9: 292 mult = 1 293 default: 294 mult = 1 295 for chunk.magnitude > 9 { 296 chunk.magnitude-- 297 chunk.v = chunk.v / 10 298 } 299 } 300 chunk.v *= mult 301 return fe.SetChunk(fieldNanos, chunk) 302 303 default: 304 return inputErrorf("cannot interpret field: %s", chunk) 305 } 306 307 case chunk.magnitude == 3 && 308 !fe.Wants(fieldYear) && fe.Wants(fieldMonth) && fe.Wants(fieldDay) && 309 chunk.v >= 1 && chunk.v <= 366: 310 // Example: 1999 001, a year and day-of-year. 311 // ^^^ 312 // Example: 1999 123 313 // ^^^ 314 // BUT NOT: 1999 1 315 return fe.SetDayOfYear(chunk) 316 317 case fe.Wants(fieldYear) && fe.Wants(fieldMonth) && fe.Wants(fieldDay): 318 var nextSep rune 319 if len(numbers) > idx+1 { 320 nextSep = numbers[idx+1].separator 321 } 322 // Example: All date formats, we're starting from scratch. 323 switch { 324 // We examine the next separator to decide if this is a 325 // concatenated date or a really long year. If it's a - or / 326 // then this is one part of a date instead of the whole date. 327 case chunk.magnitude >= 6 && chunk.separator != '-' && nextSep != '-' && nextSep != '/': 328 // Example: "YYMMDD" 329 // ^^^^^^ 330 // Example: "YYYYMMDD" 331 // ^^^^^^^^ 332 // We're looking at some kind of concatenated date. We do want 333 // to exclude large-magnitude, negative years from this test. 334 335 // Record whether or not it's a two-digit year. 336 fe.tweakYear = chunk.magnitude == 6 337 if err := fe.Set(fieldDay, chunk.v%100); err != nil { 338 return err 339 } 340 chunk.v /= 100 341 if err := fe.Set(fieldMonth, chunk.v%100); err != nil { 342 return err 343 } 344 chunk.v /= 100 345 return fe.SetChunk(fieldYear, chunk) 346 347 case chunk.magnitude >= 3 || fe.mode == ParseModeYMD: 348 // Example: "YYYY MM DD" 349 // ^^^^ 350 // Example: "YYY MM DD" 351 // ^^^ 352 // Example: "YY MM DD" 353 // ^^ 354 // A three- or four-digit number must be a year. If we are in a 355 // year-first mode, we'll accept the first chunk and possibly 356 // adjust a two-digit value later on. This means that 357 // 99 would get adjusted to 1999, but 0099 would not. 358 if chunk.separator == '-' { 359 chunk.v *= -1 360 } else if chunk.magnitude <= 2 { 361 fe.tweakYear = true 362 } 363 return fe.SetChunk(fieldYear, chunk) 364 case fe.mode == ParseModeDMY: 365 // Example: "DD MM YY" 366 // ^^ 367 // The first value is ambiguous, so we rely on the mode. 368 return fe.SetChunk(fieldDay, chunk) 369 case fe.mode == ParseModeMDY: 370 // Example: "MM DD YY" 371 // ^^ 372 // The first value is ambiguous, so we rely on the mode. 373 return fe.SetChunk(fieldMonth, chunk) 374 } 375 376 case !fe.Wants(fieldYear) && fe.Wants(fieldMonth) && fe.Wants(fieldDay): 377 // Example: "YY MM DD" 378 // ^^ 379 // Example: "YYYY MM DD" 380 // ^^ 381 // We have the year set and are looking for the month and day. 382 return fe.Set(fieldMonth, chunk.v) 383 384 case fe.Wants(fieldYear) && !fe.Wants(fieldMonth) && fe.Wants(fieldDay): 385 // Example: "YYYY Month DD" 386 // ^^^^ 387 // Example: "YYY Month DD" 388 // ^^^ 389 // Example: "MM DD YY"; only in MDY mode. 390 // ^^ 391 // Example: "Month DD YY"; only in MDY mode 392 // ^^ 393 // Example: "DD Month YY"; only in DMY mode 394 // ^^ 395 // WARNING: "YY Month DD"; OK in YMD mode. In other modes, we'll 396 // ^^ wind up storing the year in the day. 397 // This is fixed up below. 398 // The month has been set, but we don't yet have a year. If we know 399 // that the month was set in the first phase, we'll look for an 400 // obvious year or defer to the parsing mode. 401 if textMonth && (chunk.magnitude >= 3 || fe.mode == ParseModeYMD) { 402 if chunk.magnitude <= 2 { 403 fe.tweakYear = true 404 } 405 return fe.SetChunk(fieldYear, chunk) 406 } 407 return fe.SetChunk(fieldDay, chunk) 408 409 case !fe.Wants(fieldYear) && !fe.Wants(fieldMonth) && fe.Wants(fieldDay): 410 // Example: "YY MM DD" 411 // ^^ 412 // WARNING: "DD MM YYYY"; may have mis-parsed the day value 413 // ^^ ^^^^ 414 // WARNING: "DD Month YYYY"; may have mis-parsed the day value 415 // ^^ ^^^^ 416 // Example: "YY Month DD" 417 // ^^ 418 // Example: "YYYY MM DD" 419 // ^^ 420 // Example: "YYYY Month DD" 421 // ^^ 422 // We should be looking at just the day component. However, we 423 // need to handle the case where we saw a two-digit year, but 424 // we're now looking at a value that can only be a year. 425 // If this happens, we'll swap the year and day, then set the year. 426 if textMonth && chunk.magnitude >= 3 && fe.tweakYear { 427 fe.tweakYear = false 428 year, _ := fe.Get(fieldYear) 429 if err := fe.Set(fieldDay, year); err != nil { 430 return err 431 } 432 return fe.Reset(fieldYear, chunk.v) 433 } 434 return fe.SetChunk(fieldDay, chunk) 435 436 case fe.Wants(fieldYear) && fe.Wants(fieldMonth) && !fe.Wants(fieldDay): 437 // Example: "DD MM YY" 438 // ^^ 439 // Example: "DD MM YYYY" 440 // ^^ 441 // BUT NOT: "Month DD YYYY"; text month set in first pass 442 return fe.SetChunk(fieldMonth, chunk) 443 444 case fe.Wants(fieldYear) && !fe.Wants(fieldMonth) && !fe.Wants(fieldDay): 445 // Example: "MM DD YY" 446 // ^^ 447 // Example: "MM DD YYYY" 448 // ^^^^ 449 // Handle MDY, DMY formats. 450 if chunk.magnitude <= 2 { 451 fe.tweakYear = true 452 } 453 return fe.SetChunk(fieldYear, chunk) 454 455 case fe.Wants(fieldTZHour) && (chunk.separator == '-' || chunk.separator == '+'): 456 // Example: "<Time> +04[:05:06]" 457 // ^^^ 458 // Example: "<Time> +0830" 459 // ^^^^^ 460 // Example: "<Time> +083000" 461 // ^^^^^^^ 462 // Example: "<Date> +08[:05:06]"; weird, but allowable. 463 // ^^^ 464 // Example: "<Date> +0830"; weird, but allowable. 465 // ^^^^^ 466 // Example: "<Date> +083000"; weird, but allowable. 467 // ^^^^^^^ 468 // We're looking at a chunk that begins with a + or -. We 469 // know that it's not a YYY-MM-DD value, since all date 470 // extract is handled in the previous cases. Since these 471 // separators don't occur for the HH fields, it must be 472 // a timezone. 473 474 if chunk.separator == '-' { 475 fe.tzSign = -1 476 } else { 477 fe.tzSign = 1 478 } 479 480 seconds := 0 481 switch chunk.magnitude { 482 case 1, 2: 483 // H or HH 484 return fe.SetChunk(fieldTZHour, chunk) 485 case 6: 486 // HHMMSS 487 seconds = chunk.v % 100 488 chunk.v /= 100 489 fallthrough 490 case 4: 491 // HHMM, but either force seconds to 0 or use value from above. 492 if err := fe.Set(fieldTZSecond, seconds); err != nil { 493 return err 494 } 495 if err := fe.Set(fieldTZMinute, chunk.v%100); err != nil { 496 return err 497 } 498 chunk.v /= 100 499 return fe.SetChunk(fieldTZHour, chunk) 500 501 default: 502 return inputErrorf("unexpected number of digits for timezone in: %s", chunk) 503 } 504 505 case !fe.Wants(fieldTZHour) && fe.Wants(fieldTZMinute): 506 // Example: "<Time> +04:05[:06]" 507 // ^^ 508 // Example: "<Date> +08:05[:06]"; weird, but allowable. 509 // ^^ 510 // BUT NOT: "<Date> +08 HH:MM:SS" 511 // BUT NOT: "<Date> +08 HHMMSS" 512 // If we have the first part of a timezone, we're either going 513 // to see the TZ-minutes field or an HH field next. We can 514 // distinguish because the TZ-minutes field must have a 515 // colon separator. 516 if chunk.separator == ':' { 517 return fe.SetChunk(fieldTZMinute, chunk) 518 } 519 520 // The separator wasn't a colon, so we no longer except to see any 521 // timezone data. Mark the tz as completed and re-interpret the 522 // chunk. This retry only happens for valid inputs when the 523 // timezone is in the middle of a timestamp. 524 fe.has = fe.has.AddAll(tzFields) 525 fe.wanted = fe.wanted.ClearAll(tzFields) 526 return fe.interpretNumber(numbers, idx, textMonth) 527 528 case !fe.Wants(fieldTZHour) && !fe.Wants(fieldTZMinute) && fe.Wants(fieldTZSecond): 529 // Example: "<Time> +04:05:06" 530 // ^^ 531 // Example: "<Date> +08:05:06"; weird, but allowable. 532 // ^^ 533 // BUT NOT: "<Date> +08:30 HH:MM:SS" 534 // BUT NOT: "<Date> +08:30 HHMMSS" 535 // This case is exactly as the one above. 536 if chunk.separator == ':' { 537 return fe.SetChunk(fieldTZSecond, chunk) 538 } 539 540 // See the case above. 541 fe.has = fe.has.Add(fieldTZSecond) 542 fe.wanted = fe.wanted.Clear(fieldTZSecond) 543 return fe.interpretNumber(numbers, idx, textMonth) 544 545 case fe.Wants(fieldHour) && fe.Wants(fieldMinute) && fe.Wants(fieldSecond): 546 // Example: "[Date] HH:MM:SS" 547 // ^^ 548 // Example: "[Date] HHMM" 549 // ^^^^ 550 // Example: "[Date] HHMMSS" 551 // ^^^^^^ 552 // We're no longer looking for date fields at this point, and 553 // we didn't match on a separator for a timezone component. 554 // We must be looking at an hour or packed time field. 555 seconds := 0 556 switch chunk.magnitude { 557 case 1, 2: 558 // H or HH 559 return fe.SetChunk(fieldHour, chunk) 560 case 6: 561 // HHMMSS 562 seconds = chunk.v % 100 563 chunk.v /= 100 564 fallthrough 565 case 4: 566 // HHMM, but either force seconds to 0 or use value from above 567 if err := fe.Set(fieldSecond, seconds); err != nil { 568 return err 569 } 570 if err := fe.Set(fieldMinute, chunk.v%100); err != nil { 571 return err 572 } 573 chunk.v /= 100 574 return fe.SetChunk(fieldHour, chunk) 575 576 default: 577 return inputErrorf("unexpected number of digits for time in %v", chunk) 578 } 579 580 case fe.Wants(fieldMinute): 581 // Example: "HH:MM" 582 // ^^ 583 return fe.SetChunk(fieldMinute, chunk) 584 585 case fe.Wants(fieldSecond): 586 // Example: "HH:MM:SS" 587 // ^^ 588 return fe.SetChunk(fieldSecond, chunk) 589 } 590 return inputErrorf("could not parse field: %v", chunk) 591 } 592 593 // MakeDate returns a time.Time containing only the date components 594 // of the extract. 595 func (fe *fieldExtract) MakeDate() (Date, error) { 596 if fe.sentinel != nil { 597 switch *fe.sentinel { 598 case TimeInfinity: 599 return PosInfDate, nil 600 case TimeNegativeInfinity: 601 return NegInfDate, nil 602 } 603 return MakeDateFromTime(*fe.sentinel) 604 } 605 606 year, _ := fe.Get(fieldYear) 607 month, _ := fe.Get(fieldMonth) 608 day, _ := fe.Get(fieldDay) 609 return MakeDateFromTime(time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)) 610 } 611 612 // MakeTime returns only the time component of the extract. 613 // If the user provided a named timezone, as opposed 614 // to a fixed offset, we will resolve the named zone to an offset 615 // based on the best-available date information. 616 func (fe *fieldExtract) MakeTime() time.Time { 617 if fe.sentinel != nil { 618 return *fe.sentinel 619 } 620 621 ret := fe.MakeTimestamp() 622 hour, min, sec := ret.Clock() 623 _, offset := ret.Zone() 624 return time.Date(0, 1, 1, hour, min, sec, ret.Nanosecond(), time.FixedZone("", offset)) 625 } 626 627 // MakeTimestamp returns a time.Time containing all extracted information. 628 func (fe *fieldExtract) MakeTimestamp() time.Time { 629 if fe.sentinel != nil { 630 return *fe.sentinel 631 } 632 633 year, _ := fe.Get(fieldYear) 634 month, _ := fe.Get(fieldMonth) 635 day, _ := fe.Get(fieldDay) 636 hour, _ := fe.Get(fieldHour) 637 min, _ := fe.Get(fieldMinute) 638 sec, _ := fe.Get(fieldSecond) 639 nano, _ := fe.Get(fieldNanos) 640 641 return time.Date(year, time.Month(month), day, hour, min, sec, nano, fe.MakeLocation()) 642 } 643 644 // MakeLocation returns the timezone information stored in the extract, 645 // or returns the default location. 646 func (fe *fieldExtract) MakeLocation() *time.Location { 647 tzHour, ok := fe.Get(fieldTZHour) 648 if !ok { 649 return fe.now.Location() 650 } 651 tzMin, _ := fe.Get(fieldTZMinute) 652 tzSec, _ := fe.Get(fieldTZSecond) 653 654 tzHour *= fe.tzSign 655 tzMin *= fe.tzSign 656 tzSec *= fe.tzSign 657 658 return zoneCacheInstance.FixedZone(tzHour, tzMin, tzSec) 659 } 660 661 // Certain keywords should result in some kind of sentinel value, 662 // but we want to ensure that we accept only a single sentinel chunk. 663 func (fe *fieldExtract) matchedSentinel(value time.Time, match string) error { 664 if fe.sentinel != nil { 665 return inputErrorf("unexpected input: %s", match) 666 } 667 fe.sentinel = &value 668 return nil 669 } 670 671 // Reset replaces a value of an already-set field. 672 func (fe *fieldExtract) Reset(field field, v int) error { 673 if !fe.has.Has(field) { 674 return errors.AssertionFailedf("field %s is not already set", errors.Safe(field.Pretty())) 675 } 676 fe.data[field] = v 677 return nil 678 } 679 680 // Set updates the value of the given field, returning an error if 681 // the field has already been set. 682 func (fe *fieldExtract) Set(field field, v int) error { 683 if !fe.wanted.Has(field) { 684 return errors.AssertionFailedf("field %s is not wanted in %v", errors.Safe(field.Pretty()), errors.Safe(fe.wanted)) 685 } 686 fe.data[field] = v 687 fe.has = fe.has.Add(field) 688 fe.wanted = fe.wanted.Clear(field) 689 690 return nil 691 } 692 693 // SetChunk first validates that the separator in the chunk is appropriate 694 // for the field being set. That is, you'd never see "YYYY:MM:DD"; 695 // colons are only appropriate for hours and minutes. 696 func (fe *fieldExtract) SetChunk(field field, chunk numberChunk) error { 697 // A space rune will be used for the initial chunk. 698 switch field { 699 case fieldYear: 700 switch chunk.separator { 701 case '-', '/', ' ', ',': 702 // DD-MM-YY 703 // DD/MM/YY 704 // DD MM YY 705 // Month DD, YYYY 706 return fe.Set(field, chunk.v) 707 } 708 case fieldMonth, fieldDay: 709 switch chunk.separator { 710 case '-', '/', ' ': 711 // DD-MM-YY 712 // DD/MM/YY 713 // DD MM YY 714 // Month DD, YYYY 715 return fe.Set(field, chunk.v) 716 } 717 case fieldTZHour: 718 switch chunk.separator { 719 case '-', '+': 720 // HH:MM:SS+04 721 // HH:MM:SS-04 722 return fe.Set(field, chunk.v) 723 } 724 case fieldHour: 725 switch chunk.separator { 726 case ' ', 't': 727 // YYYY-MM-DD HH:MM:SS 728 // yyyymmddThhmmss 729 return fe.Set(field, chunk.v) 730 } 731 case fieldMinute, fieldSecond, fieldTZMinute, fieldTZSecond: 732 if chunk.separator == ':' { 733 // HH:MM:SS 734 return fe.Set(field, chunk.v) 735 } 736 case fieldNanos: 737 if chunk.separator == '.' { 738 // HH:MM:SS.NNNNNNNNN 739 return fe.Set(field, chunk.v) 740 } 741 } 742 return badFieldPrefixError(field, chunk.separator) 743 } 744 745 // SetDayOfYear updates the month and day fields to reflect the 746 // given day-of-year. The year must have been previously set. 747 func (fe *fieldExtract) SetDayOfYear(chunk numberChunk) error { 748 if chunk.separator != ' ' && chunk.separator != '.' { 749 return badFieldPrefixError(fieldMonth, chunk.separator) 750 } 751 752 y, ok := fe.Get(fieldYear) 753 if !ok { 754 return errors.AssertionFailedf("year must be set before day of year") 755 } 756 y, m, d := julianDayToDate(dateToJulianDay(y, 1, 1) + chunk.v - 1) 757 if err := fe.Reset(fieldYear, y); err != nil { 758 return err 759 } 760 if err := fe.Set(fieldMonth, m); err != nil { 761 return err 762 } 763 return fe.Set(fieldDay, d) 764 } 765 766 func (fe *fieldExtract) String() string { 767 ret := "[ " 768 for f := fieldMinimum; f <= fieldMaximum; f++ { 769 if v, ok := fe.Get(f); ok { 770 ret += fmt.Sprintf("%s: %d ", f.Pretty(), v) 771 } 772 } 773 ret += "]" 774 return ret 775 } 776 777 // validate ensures that the data in the extract is reasonable. It also 778 // performs some field fixups, such as converting two-digit years 779 // to actual values and adjusting for AM/PM. 780 func (fe *fieldExtract) validate() error { 781 // If we have any of the required fields, we must have all of the required fields. 782 if fe.has.HasAny(dateRequiredFields) && !fe.has.HasAll(dateRequiredFields) { 783 return inputErrorf("missing required date fields") 784 } 785 if fe.has.HasAny(timeRequiredFields) && !fe.has.HasAll(timeRequiredFields) { 786 return inputErrorf("missing required time fields") 787 } 788 if !fe.has.HasAll(fe.required) { 789 return inputErrorf("missing required fields in input") 790 } 791 792 if year, ok := fe.Get(fieldYear); ok { 793 if year == 0 { 794 return outOfRangeError("year", year) 795 } 796 // Update for BC dates. 797 if era, ok := fe.Get(fieldEra); ok && era < 0 { 798 // No year 0 799 if year <= 0 { 800 return inputErrorf("no year 0 in AD/BC notation") 801 } 802 // Normalize to a negative year 803 if err := fe.Reset(fieldYear, 1-year); err != nil { 804 return err 805 } 806 } else if fe.tweakYear { 807 if year < 0 { 808 return inputErrorf("negative year not allowed") 809 } 810 if year < 70 { 811 year += 2000 812 } else if year < 100 { 813 year += 1900 814 } 815 if err := fe.Reset(fieldYear, year); err != nil { 816 return err 817 } 818 } 819 820 if month, ok := fe.Get(fieldMonth); ok { 821 if month < 1 || month > 12 { 822 return outOfRangeError("month", month) 823 } 824 825 if day, ok := fe.Get(fieldDay); ok { 826 var maxDay int 827 if isLeap(year) { 828 maxDay = daysInMonth[1][month] 829 } else { 830 maxDay = daysInMonth[0][month] 831 } 832 if day < 1 || day > maxDay { 833 return outOfRangeError("day", day) 834 } 835 } 836 } 837 } 838 839 if hour, ok := fe.Get(fieldHour); ok { 840 hasDate := fe.has.HasAny(dateRequiredFields) 841 842 meridian, _ := fe.Get(fieldMeridian) 843 switch meridian { 844 case fieldValueAM: 845 switch { 846 case hour < 0 || hour > 12: 847 return outOfRangeError("hour", hour) 848 case hour == 12: 849 if err := fe.Reset(fieldHour, 0); err != nil { 850 return err 851 } 852 } 853 854 case fieldValuePM: 855 switch { 856 case hour < 0 || hour > 12: 857 return outOfRangeError("hour", hour) 858 case hour == 12: 859 // 12 PM -> 12 860 default: 861 // 1 PM -> 13 862 if err := fe.Reset(fieldHour, hour+12); err != nil { 863 return err 864 } 865 } 866 867 default: 868 // 24:00:00 is the maximum-allowed value 869 if hour < 0 || (hasDate && hour > 24) || (!hasDate && hour > 23) { 870 return outOfRangeError("hour", hour) 871 } 872 } 873 874 minute, _ := fe.Get(fieldMinute) 875 if minute < 0 || minute > 59 { 876 return outOfRangeError("minute", minute) 877 } 878 879 second, _ := fe.Get(fieldSecond) 880 if second < 0 || (hasDate && second > 60) || (!hasDate && second > 59) { 881 return outOfRangeError("second", second) 882 } 883 884 nanos, _ := fe.Get(fieldNanos) 885 if nanos < 0 { 886 return outOfRangeError("nanos", nanos) 887 } 888 889 x := time.Duration(hour)*time.Hour + 890 time.Duration(minute)*time.Minute + 891 time.Duration(second)*time.Second + 892 time.Duration(nanos)*time.Nanosecond 893 if x > 24*time.Hour { 894 return inputErrorf("time out of range: %d", x) 895 } 896 } 897 898 return nil 899 } 900 901 // Wants returns whether or not the field is wanted in the extract. 902 func (fe *fieldExtract) Wants(field field) bool { 903 return fe.wanted.Has(field) 904 }