diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 011edf4..12855d8 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -4,17 +4,27 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: - go-version: 1.21.x - - uses: actions/checkout@v3 - - uses: golangci/golangci-lint-action@v3 + go-version: 1.23.x + - uses: actions/checkout@v4 + - uses: golangci/golangci-lint-action@v6 test: name: test runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 + with: + go-version: 1.21.x + - uses: actions/checkout@v4 + - run: go test -v -count 1 -tags parse ./... + integration: + name: integration + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v5 with: go-version: 1.21.x - - uses: actions/checkout@v2 - - run: go test -v -count 1 ./... + - uses: actions/checkout@v4 + - run: go run main.go + working-directory: ./test/integration diff --git a/.vscode/settings.json b/.vscode/settings.json index 9012f14..69faa0c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "go.lintTool": "golangci-lint" + "go.lintTool": "golangci-lint", + "go.buildTags": "parse" } \ No newline at end of file diff --git a/README.md b/README.md index 024a268..e2677e9 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,28 @@ And parsing a time: ```golang var time chrono.LocalTime -time.Parse("%H:%M:%S", "12:30:15") +fmt.Println(time.Parse("%H:%M:%S", "12:30:15")) ``` There are also predefined layouts, similar to the `time` package, but with the addition of layouts compatible with ISO 8601. +### Experimental: Parsing without a layout + +The example above assumes that you know how a date time string is formatted, but that's not always the case. For these situations, `ParseToLayout` accepts just a string and attempts to parse it, also returning the layout string. + +```golang +var c chrono.OffsetDateTime +fmt.Println(chrono.ParseToLayout( + "2006-04-09", + chrono.ParseConfig{}, + &c, +)) // %Y-%m-%d +``` + +To access this function you need to build with `-tag parse`. + +*This API is incomplete and subject to change until a stable release is reached.* + ## Parse and format ISO 8601 durations When interfacing with systems where the time package's duration formatting is not understood, ISO 8601 is a commonly-adopted standard. diff --git a/duration_test.go b/duration_test.go index 85f588f..954d231 100644 --- a/duration_test.go +++ b/duration_test.go @@ -359,11 +359,11 @@ func TestDuration_Parse(t *testing.T) { } } - t.Run("dots", func(t *testing.T) { + t.Run("dots", func(_ *testing.T) { run() }) - t.Run("commas", func(t *testing.T) { + t.Run("commas", func(_ *testing.T) { tt.input = strings.ReplaceAll(tt.input, ".", ",") run() }) diff --git a/extent_test.go b/extent_test.go index d6287f3..6c16b82 100644 --- a/extent_test.go +++ b/extent_test.go @@ -89,11 +89,11 @@ func TestExtent_Parse(t *testing.T) { } } - t.Run("dots", func(t *testing.T) { + t.Run("dots", func(_ *testing.T) { run() }) - t.Run("commas", func(t *testing.T) { + t.Run("commas", func(_ *testing.T) { tt.input = strings.ReplaceAll(tt.input, ".", ",") run() }) diff --git a/format.go b/format.go index 76a64b3..cba512f 100644 --- a/format.go +++ b/format.go @@ -60,13 +60,13 @@ import ( // For example, '%m' may produce the string '04' (for March), but '%-m' produces '4'. // However, when parsing using these specifiers, it is not required that the input string contains any leading zeros. // -// When parsing using specifier that represent textual values (month names, etc.), the input text is treated case insensitively. +// When parsing using specifiers that represent textual values (e.g. month names, etc.), the input text is treated case insensitively. // // Depending on the context in which the layout is used, only a subset of specifiers may be supported by a particular function. // For example, %H is not supported when parsing or formatting a date. // // When parsing, if multiple instances of the same specifier, or multiple instances of a specifier that represent the same value, -// are encountered, only the instance will be considered. See note (2). +// are encountered, only the last instance will be considered. See note (2). // // If a specifier is encountered which is not recognized (defined in the list above), or not supported by a particular function, // the function will panic with a message that includes the unrecognized sequence. @@ -163,11 +163,11 @@ NextChar: return "", err } - decimal := func(v int, len int) string { + decimal := func(v int, n int) string { if nopad { return strconv.Itoa(v) } - return fmt.Sprintf("%0*d", len, v) + return fmt.Sprintf("%0*d", n, v) } switch { @@ -311,6 +311,36 @@ const ( endOfStringErrMsg = "parsing time \"%s\": end of string" ) +type parts struct { + haveDate bool + haveGregorianYear bool + isBCE bool + year int + yearCentury *int + shortYear *int + yearType int // -1 = short/century, 0 = none, 1 = full year + + month int + day int + + dayOfWeek int + + dayOfYear int + + haveISODate bool + isoYear int + isoWeek int + + have12HourClock bool + isAfternoon bool + hour int + min int + sec int + nsec int + + offset int64 +} + // parseDateAndTime parses the supplied value according to the specified layout. // date, time and offset must be provided in order for those components to be parsed. // If not provided, and the specifiers that pertain to those components are @@ -318,48 +348,26 @@ const ( // If non-zero, date, time, and offset and taken as starting points, where the individual values // that they represent are replaced only if present in the supplied layout. func parseDateAndTime(layout, value string, date, time, offset *int64) error { - var ( - haveDate bool - haveGregorianYear bool - isBCE bool - year int - yearCentury *int - shortYear *int - yearType int // -1 = short/century, 0 = none, 1 = full year - - month int - day int - - dayOfWeek int - - dayOfYear int - - haveISODate bool - isoYear int - isoWeek int - - have12HourClock bool - isAfternoon bool - hour int - min int - sec int - nsec int - ) + var parts parts var err error if date != nil { - if year, month, day, err = fromDate(*date); err != nil { + if parts.year, parts.month, parts.day, err = fromDate(*date); err != nil { return err } - if isoYear, isoWeek, err = getISOWeek(*date); err != nil { + if parts.isoYear, parts.isoWeek, err = getISOWeek(*date); err != nil { return err } } if time != nil { - hour, min, sec, nsec = fromTime(*time) - _, isAfternoon = convert24To12HourClock(hour) + parts.hour, parts.min, parts.sec, parts.nsec = fromTime(*time) + _, parts.isAfternoon = convert24To12HourClock(parts.hour) + } + + if offset != nil { + parts.offset = *offset } var pos int @@ -495,35 +503,35 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { case date != nil && main == 'a': // %a lower, original := alphas(3) var ok bool - if dayOfWeek, ok = shortDayNameLookup[lower]; !ok { + if parts.dayOfWeek, ok = shortDayNameLookup[lower]; !ok { return fmt.Errorf("unrecognized short day name %q", original) } case date != nil && main == 'A': // %A lower, original := alphas(9) var ok bool - if dayOfWeek, ok = longDayNameLookup[lower]; !ok { + if parts.dayOfWeek, ok = longDayNameLookup[lower]; !ok { return fmt.Errorf("unrecognized day name %q", original) } case date != nil && main == 'b': // %b lower, original := alphas(3) var ok bool - if month, ok = shortMonthNameLookup[lower]; !ok { + if parts.month, ok = shortMonthNameLookup[lower]; !ok { return fmt.Errorf("unrecognized short month name %q", original) } case date != nil && main == 'B': // %B lower, original := alphas(9) var ok bool - if month, ok = longMonthNameLookup[lower]; !ok { + if parts.month, ok = longMonthNameLookup[lower]; !ok { return fmt.Errorf("unrecognized month name %q", original) } case date != nil && main == 'C': if localed { // %EC - haveGregorianYear = true + parts.haveGregorianYear = true lower, original := alphas(3) switch lower { case "ce": case "bce": - isBCE = true + parts.isBCE = true default: return fmt.Errorf("unrecognized era %q", original) } @@ -532,12 +540,12 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { if v, err = integer(2); err != nil { return err } - yearCentury = &v - yearType = -1 + parts.yearCentury = &v + parts.yearType = -1 } case date != nil && main == 'd': // %d - haveDate = true - if day, err = integer(2); err != nil { + parts.haveDate = true + if parts.day, err = integer(2); err != nil { return err } case time != nil && main == 'f': // %f @@ -551,43 +559,43 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { if err != nil { return err } - nsec = millis * 1000000 + parts.nsec = millis * 1000000 case 6: // %6f micros, err := integer(6) if err != nil { return err } - nsec = micros * 1000 + parts.nsec = micros * 1000 case 9: // %9f - if nsec, err = integer(9); err != nil { + if parts.nsec, err = integer(9); err != nil { return err } default: } case date != nil && main == 'G': // %G - haveISODate = true - if isoYear, err = integer(4); err != nil { + parts.haveISODate = true + if parts.isoYear, err = integer(4); err != nil { return err } case time != nil && main == 'H': // %H - if hour, err = integer(2); err != nil { + if parts.hour, err = integer(2); err != nil { return err } case time != nil && main == 'I': // %I - have12HourClock = true - if hour, err = integer(2); err != nil { + parts.have12HourClock = true + if parts.hour, err = integer(2); err != nil { return err } case date != nil && main == 'j': // %j - if dayOfYear, err = integer(3); err != nil { + if parts.dayOfYear, err = integer(3); err != nil { return err } case date != nil && main == 'm': // %m - if month, err = integer(2); err != nil { + if parts.month, err = integer(2); err != nil { return err } case time != nil && main == 'M': // %M - if min, err = integer(2); err != nil { + if parts.min, err = integer(2); err != nil { return err } case time != nil && main == 'p': // %p @@ -595,7 +603,7 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { switch strings.ToUpper(lower) { case "AM": case "PM": - isAfternoon = true + parts.isAfternoon = true default: return fmt.Errorf("failed to parse time of day %q", original) } @@ -604,43 +612,43 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { switch lower { case "am": case "pm": - isAfternoon = true + parts.isAfternoon = true default: return fmt.Errorf("failed to parse time of day %q", original) } case time != nil && main == 'S': // %S - if sec, err = integer(2); err != nil { + if parts.sec, err = integer(2); err != nil { return err } case date != nil && main == 'u': // %u - if dayOfWeek, err = integer(1); err != nil { + if parts.dayOfWeek, err = integer(1); err != nil { return err } case date != nil && main == 'V': // %V - haveISODate = true - if isoWeek, err = integer(2); err != nil { + parts.haveISODate = true + if parts.isoWeek, err = integer(2); err != nil { return err } case date != nil && main == 'y': // %y if localed { // %Ey - haveGregorianYear = true + parts.haveGregorianYear = true } var v int if v, err = integer(2); err != nil { return err } - shortYear = &v - yearType = -1 + parts.shortYear = &v + parts.yearType = -1 case date != nil && main == 'Y': // %Y if localed { // %EY - haveGregorianYear = true + parts.haveGregorianYear = true } - if year, err = integer(4); err != nil { + if parts.year, err = integer(4); err != nil { return err } - yearType = 1 + parts.yearType = 1 case time != nil && main == 'z': // %z // If at end of input and no offset is requested, break. // But continue to parse in the case where offset is not requested, but may be present. @@ -686,7 +694,7 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { // Parsing %z into a type that contains no offset (e.g. LocalTime, LocalDateTime) // is valid, although the value itself is ignored. But it needed to be consumed above, just now discarded. if offset != nil { - *offset = v + parts.offset = v } case main == '%': // %% default: @@ -742,41 +750,46 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { return fmt.Errorf(extraTextErrMsg, value, value[pos:]) } + return applyParts(parts, date, time, offset) +} + +func applyParts(parts parts, date, time, offset *int64) error { if date != nil { // Check century according to note (9). - if yearCentury != nil { - if yearType == 1 && year/100 != *yearCentury { - return fmt.Errorf("year century %d does not agree with year %d", *yearCentury, year) - } else if yearType != 1 { - year = *yearCentury * 100 + if parts.yearCentury != nil { + if parts.yearType == 1 && parts.year/100 != *parts.yearCentury { + return fmt.Errorf("year century %d does not agree with year %d", *parts.yearCentury, parts.year) + } else if parts.yearType != 1 { + parts.year = *parts.yearCentury * 100 } } // Check 2-digit year according to note (9). - if shortYear != nil { - _year := getCentury(*shortYear) + *shortYear - if yearCentury != nil { - _year = *yearCentury*100 + *shortYear + if parts.shortYear != nil { + _year := getCentury(*parts.shortYear) + *parts.shortYear + if parts.yearCentury != nil { + _year = *parts.yearCentury*100 + *parts.shortYear } - if yearType == 1 && year-(year/100*100) != *shortYear { - return fmt.Errorf("short year %d (%d) does not agree with year %d", *shortYear, _year, year) - } else if yearType != 1 { - year = _year + if parts.yearType == 1 && parts.year-(parts.year/100*100) != *parts.shortYear { + return fmt.Errorf("short year %d (%d) does not agree with year %d", *parts.shortYear, _year, parts.year) + } else if parts.yearType != 1 { + parts.year = _year } } - if haveGregorianYear { - if year, err = convertGregorianToISOYear(year, isBCE); err != nil { + if parts.haveGregorianYear { + var err error + if parts.year, err = convertGregorianToISOYear(parts.year, parts.isBCE); err != nil { return err } } - if !isDateValid(year, month, day) { - return fmt.Errorf("invalid date %q", simpleDateStr(year, month, day)) + if !isDateValid(parts.year, parts.month, parts.day) { + return fmt.Errorf("invalid date %q", simpleDateStr(parts.year, parts.month, parts.day)) } - _date, err := makeDate(year, month, day) + _date, err := makeDate(parts.year, parts.month, parts.day) if err != nil { return err } @@ -784,16 +797,16 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { *date = _date // Check day of year according to note (2). - if dayOfYear != 0 { - doyDate, err := ofDayOfYear(year, dayOfYear) + if parts.dayOfYear != 0 { + doyDate, err := ofDayOfYear(parts.year, parts.dayOfYear) if err != nil { return err } - if haveDate && (doyDate != _date) { + if parts.haveDate && (doyDate != _date) { return fmt.Errorf("day-of-year date %q does not agree with date %q", LocalDate(doyDate).String(), - simpleDateStr(year, month, day), + simpleDateStr(parts.year, parts.month, parts.day), ) } @@ -801,21 +814,21 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { } // Check ISO week-year according to note (2). - if haveISODate { - weekday := dayOfWeek - if dayOfWeek == 0 { + if parts.haveISODate { + weekday := parts.dayOfWeek + if parts.dayOfWeek == 0 { weekday = int(Monday) } - isoDate, err := ofISOWeek(isoYear, isoWeek, weekday) + isoDate, err := ofISOWeek(parts.isoYear, parts.isoWeek, weekday) if err != nil { - return fmt.Errorf("invalid ISO week-year date %q", getISODateSimpleStr(isoYear, isoWeek, day)) + return fmt.Errorf("invalid ISO week-year date %q", getISODateSimpleStr(parts.isoYear, parts.isoWeek, parts.day)) } - if haveDate && (isoDate != _date) { + if parts.haveDate && (isoDate != _date) { return fmt.Errorf("ISO week-year date %q does not agree with date %q", - getISODateSimpleStr(isoYear, isoWeek, day), - simpleDateStr(year, month, day), + getISODateSimpleStr(parts.isoYear, parts.isoWeek, parts.day), + simpleDateStr(parts.year, parts.month, parts.day), ) } @@ -823,11 +836,11 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { } // Check day of week according to note (3). - haveDate = haveDate || dayOfYear != 0 - if dayOfWeek != 0 && haveDate { - if actual := getWeekday(int32(*date)); dayOfWeek != actual { + parts.haveDate = parts.haveDate || parts.dayOfYear != 0 + if parts.dayOfWeek != 0 && parts.haveDate { + if actual := getWeekday(int32(*date)); parts.dayOfWeek != actual { return fmt.Errorf("day of week %q does not agree with actual day of week %q", - longWeekdayName(dayOfWeek), + longWeekdayName(parts.dayOfWeek), longWeekdayName(actual), ) } @@ -836,20 +849,24 @@ func parseDateAndTime(layout, value string, date, time, offset *int64) error { if time != nil { // Check validity of hour on 12-hour clock according to note (5). - if have12HourClock { - if hour < 1 || hour > 12 { - return fmt.Errorf("hour %d is not valid on the 12-hour clock", hour) + if parts.have12HourClock { + if parts.hour < 1 || parts.hour > 12 { + return fmt.Errorf("hour %d is not valid on the 12-hour clock", parts.hour) } - hour = convert12To24HourClock(hour, isAfternoon) + parts.hour = convert12To24HourClock(parts.hour, parts.isAfternoon) } - v, err := makeTime(hour, min, sec, nsec) + v, err := makeTime(parts.hour, parts.min, parts.sec, parts.nsec) if err != nil { return err } *time = v } + if offset != nil { + *offset = parts.offset + } + return nil } diff --git a/offset_date_time.go b/offset_date_time.go index 06f04ea..aee8b83 100644 --- a/offset_date_time.go +++ b/offset_date_time.go @@ -153,7 +153,16 @@ func (d *OffsetDateTime) Parse(layout, value string) error { return err } + d.set(dv, tv, ov) + return nil +} + +func (d OffsetDateTime) get() (dv, tv, ov *int64) { + _dv, _tv := splitDateAndTime(d.v) + return &_dv, &_tv, &d.o +} + +func (d *OffsetDateTime) set(dv, tv, ov int64) { d.v = makeDateTime(dv, tv) d.o = ov - return nil } diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..4290d9b --- /dev/null +++ b/parse.go @@ -0,0 +1,365 @@ +//go:build parse + +package chrono + +import ( + "strconv" + "strings" + "unicode" +) + +type ParseConfig struct { + DayFirst bool +} + +type Chronological interface { + String() string + Parse(layout, value string) error + get() (dv, tv, ov *int64) + set(dv, tv, ov int64) +} + +// func Parse(value string, conf ParseConfig) (Chronological, error) { +// // pick type + +// return nil, nil +// } + +// ParseToLayout attempts to parse the input string as C and returns +// the applicable layout string that would parse that string, or format C to that string. +// Any valid and non-ambiguous ISO 8601 string will be parsed correctly, +// and any other string will be parsed with a best effort attempt, although the resulting +// layout string may not be valid. +func ParseToLayout(value string, conf ParseConfig, c Chronological) (string, error) { + var ( + typ, prevTyp rune // a = alpha, n = numeric, s = separator, w = whitespace, o = other + sign int // -1 = negative, 0 = no sign, 1 = positive + buf []rune + + state state + layout []string + parts parts + ambiguous []chunk + ) + + var date, time, offset *int64 + if c != nil { + date, time, offset = c.get() + } + + var err error + if date != nil { + if parts.year, parts.month, parts.day, err = fromDate(*date); err != nil { + return "", err + } + + if parts.isoYear, parts.isoWeek, err = getISOWeek(*date); err != nil { + return "", err + } + } + + if time != nil { + parts.hour, parts.min, parts.sec, parts.nsec = fromTime(*time) + _, parts.isAfternoon = convert24To12HourClock(parts.hour) + } + + if offset != nil { + parts.offset = *offset + } + + _ = sign // TODO + + for i, c := range []rune(value) { + switch { + case (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'): + typ = 'a' + case c >= '0' && c <= '9': + typ = 'n' + case (c == '+' || c == '-') && (prevTyp == 0 || prevTyp != 'n'): + typ = 'n' + case c == '/' || c == '-' || c == '.' || c == ',' || c == ':': + typ = 's' + case unicode.IsSpace(c): + typ = 'w' + default: + typ = 'o' + } + + if typ == prevTyp { + buf = append(buf, c) + + if i != len(value)-1 { + continue + } + } + + evalParseRules(prevTyp, buf, conf, &state, &layout, &parts, &ambiguous) + + prevTyp = typ + typ = 0 + sign = 0 + buf = []rune{c} + } + + //fmt.Println("=>", layout) + + evalAmbiguous(ambiguous, conf, layout, &parts) + + layoutStr := strings.Join(layout, "") + + if c != nil { + if err := applyParts(parts, date, time, offset); err != nil { + return layoutStr, err + } + + c.set(*date, *time, *offset) + } + + return layoutStr, nil +} + +func evalParseRules(prevTyp rune, buf []rune, conf ParseConfig, state *state, layout *[]string, parts *parts, ambiguous *[]chunk) { + switch prevTyp { + case 'w', 'o': + *layout = append(*layout, string(buf)) + case 's': + switch buf[0] { + case '-': + state.component = componentDate + case '/': + state.component = componentDate + // TODO conf + case ':': + state.component = componentTime + } + *layout = append(*layout, string(buf)) + case 'n': + var sign int + switch buf[0] { + case '+': + sign = 1 + case '-': + sign = -1 + } + + v, err := strconv.Atoi(string(buf)) + if err != nil { + panic(err) + } + + if str, ok := eval(v, sign, uint(len(buf)), conf, state, parts, false); ok { + *layout = append(*layout, str) + + if len(*ambiguous) != 0 { + (*ambiguous)[len(*ambiguous)-1].state = *state + } + + return + } + + *layout = append(*layout, string(buf)) + *ambiguous = append(*ambiguous, chunk{ + pos: uint(len(*layout) - 1), + v: v, + sign: sign, + digits: uint(len(buf)), + }) + } +} + +func evalAmbiguous(chunks []chunk, conf ParseConfig, layout []string, parts *parts) { + for i := len(chunks) - 1; i >= 0; i-- { + if str, ok := eval(chunks[i].v, chunks[i].sign, chunks[i].digits, conf, &chunks[i].state, parts, true); ok { + layout[chunks[i].pos] = str + } + } +} + +type state struct { + component + datePart + timePart +} + +func (s state) String() string { + return string(s.component) + string(s.datePart) + string(s.timePart) +} + +func (s state) d(rev bool) datePart { + if rev && s.datePart == datePartYear { + return datePartDay + } else if rev && s.datePart == datePartDay { + return datePartYear + } + return s.datePart +} + +func (s state) t(rev bool) timePart { + return s.timePart // TODO +} + +type component rune + +const ( + componentNone = 0 + componentDate component = 'd' + componentTime component = 't' +) + +type datePart rune + +const ( + datePartNone = 0 + datePartYear datePart = 'y' + datePartMonth datePart = 'm' + datePartDay datePart = 'd' +) + +type timePart rune + +const ( + timePartHour timePart = 'h' + timePartMinute timePart = 'm' + timePartSecond timePart = 's' +) + +type chunk struct { + pos uint + v int + sign int + digits uint + state state +} + +func eval(v int, sign int, digits uint, conf ParseConfig, state *state, parts *parts, rev bool) (str string, ok bool) { + // fmt.Println("========= eval") + // fmt.Println("v:", v) + // fmt.Println("sign:", sign) + // fmt.Println("digits:", digits) + // fmt.Println("conf:", conf) + // fmt.Println("state:", *state) + // fmt.Println("datePart:", string(state.d(rev))) + // fmt.Println("parts:", *parts) + // fmt.Println("rev:", rev) + + // defer func() { + // fmt.Println("out:", ok, str) + // }() + + switch state.component { + case componentNone: + switch digits { + case 4: // [2006](-01-02) => %Y + if sign == 0 { + sign = 1 + } + + state.component = componentDate + state.datePart = datePartYear + parts.year = v * sign + return "%Y", true + } + case componentDate: + switch d := state.d(rev); d { + case datePartNone: + switch digits { + case 2: + switch conf.DayFirst { + case false: + switch sign { + case 0: // 01-[02] => %d + state.component = componentDate + state.datePart = datePartDay + parts.day = v + return "%d", true + } + case true: + switch sign { + case 0: // 02-[01](-2006) => %m + state.component = componentDate + state.datePart = datePartMonth + parts.month = v + return "%m", true + } + } + case 4: // 02-[2006] => %Y + if sign == 0 { + sign = 1 + } + + state.component = componentDate + state.datePart = datePartYear + parts.year = v * sign + return "%Y", true + } + case datePartYear: + switch digits { + case 2: + switch conf.DayFirst { + case false: + switch sign { + case 0: // 2006-[01](-02) => %m + state.component = componentDate + state.datePart = datePartMonth + parts.month = v + return "%m", true + } + case true: + switch sign { + case 0: // 2006-[02](-01) => %d + state.component = componentDate + state.datePart = datePartDay + parts.day = v + return "%d", true + } + } + } + case datePartMonth: + switch digits { + case 2: + switch conf.DayFirst { + case false: + switch sign { + case 0: // (2006)-01-[02] => %d + state.component = componentDate + state.datePart = datePartDay + parts.day = v + return "%d", true + } + case true: + switch sign { + case 0: // // [02]-01(-2006) => %m + state.component = componentDate + state.datePart = datePartDay + parts.day = v + return "%d", true + } + } + } + case datePartDay: + switch digits { + case 2: + switch conf.DayFirst { + case true: + switch sign { + case 0: // 2006-02-[01] => %m + state.component = componentDate + state.datePart = datePartMonth + parts.month = v + return "%m", true + } + case false: + switch sign { + case 0: // [01]-2006 => %m + state.component = componentDate + state.datePart = datePartMonth + parts.month = v + return "%m", true + } + } + } + } + } + + return "", false +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..097e18b --- /dev/null +++ b/parse_test.go @@ -0,0 +1,61 @@ +//go:build parse + +package chrono_test + +import ( + "testing" + + "github.com/go-chrono/chrono" +) + +func TestParseToLayout(t *testing.T) { + t.Run("OffsetDateTime", func(t *testing.T) { + for _, tt := range []struct { + name string + value string + conf chrono.ParseConfig + expectedC chrono.OffsetDateTime + expectedLayout string + expectedErr error + }{ + {"%Y-%m-%d", "2006-04-09", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 4, 9, 0, 0, 0, 0, 0, 0), "%Y-%m-%d", nil}, + {"%Y-%m", "2006-04", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 4, 1, 0, 0, 0, 0, 0, 0), "%Y-%m", nil}, + {"%Y", "2006", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 1, 1, 0, 0, 0, 0, 0, 0), "%Y", nil}, + {"%Y-%d", "2006-09", chrono.ParseConfig{DayFirst: true}, chrono.OffsetDateTimeOf(2006, 1, 9, 0, 0, 0, 0, 0, 0), "%Y-%d", nil}, + {"%m-%Y", "04-2006", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 4, 1, 0, 0, 0, 0, 0, 0), "%m-%Y", nil}, + {"%m-%d", "04-09", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(1970, 4, 9, 0, 0, 0, 0, 0, 0), "%m-%d", nil}, + {"%d-%m-%Y", "09-04-2006", chrono.ParseConfig{DayFirst: true}, chrono.OffsetDateTimeOf(2006, 4, 9, 0, 0, 0, 0, 0, 0), "%d-%m-%Y", nil}, + {"%d-%m", "09-04", chrono.ParseConfig{DayFirst: true}, chrono.OffsetDateTimeOf(1970, 4, 9, 0, 0, 0, 0, 0, 0), "%d-%m", nil}, + {"%Y-%d-%m", "2006-09-04", chrono.ParseConfig{DayFirst: true}, chrono.OffsetDateTimeOf(2006, 4, 9, 0, 0, 0, 0, 0, 0), "%Y-%d-%m", nil}, + {"%Y-%d", "2006-09", chrono.ParseConfig{DayFirst: true}, chrono.OffsetDateTimeOf(2006, 1, 9, 0, 0, 0, 0, 0, 0), "%Y-%d", nil}, + {"%Y", "2006", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 1, 1, 0, 0, 0, 0, 0, 0), "%Y", nil}, + {"%Y-%m", "2006-04", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 4, 1, 0, 0, 0, 0, 0, 0), "%Y-%m", nil}, + {"%d-%m", "09-04", chrono.ParseConfig{DayFirst: true}, chrono.OffsetDateTimeOf(1970, 4, 9, 0, 0, 0, 0, 0, 0), "%d-%m", nil}, + {"%m-%d-%Y", "04-09-2006", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 4, 9, 0, 0, 0, 0, 0, 0), "%m-%d-%Y", nil}, + {"%m-%d", "04-09", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(1970, 4, 9, 0, 0, 0, 0, 0, 0), "%m-%d", nil}, + {"%m-%Y", "04-2006", chrono.ParseConfig{}, chrono.OffsetDateTimeOf(2006, 4, 1, 0, 0, 0, 0, 0, 0), "%m-%Y", nil}, + } { + t.Run(tt.name, func(t *testing.T) { + var c chrono.OffsetDateTime + + if actual, err := chrono.ParseToLayout(tt.value, tt.conf, &c); err != nil { + if tt.expectedErr == nil { + t.Errorf("unexpected error: %v", err) + } else if err.Error() != tt.expectedErr.Error() { + t.Errorf("got unexpected error %v, want %v", err, tt.expectedErr) + } + } else { + if tt.expectedErr != nil { + t.Errorf("expecting error %v but got nil", tt.expectedErr) + } else if actual != tt.expectedLayout { + t.Errorf("got %q, want %q", actual, tt.expectedLayout) + } + } + + if c.Compare(tt.expectedC) != 0 { + t.Errorf("got %q, want %q", c, tt.expectedC) + } + }) + } + }) +} diff --git a/period_test.go b/period_test.go index 6ff6a6d..674b4ac 100644 --- a/period_test.go +++ b/period_test.go @@ -212,11 +212,11 @@ func TestParseDuration(t *testing.T) { } } - t.Run("dots", func(t *testing.T) { + t.Run("dots", func(_ *testing.T) { run() }) - t.Run("commas", func(t *testing.T) { + t.Run("commas", func(_ *testing.T) { tt.input = strings.ReplaceAll(tt.input, ".", ",") run() }) diff --git a/test/integration/go.mod b/test/integration/go.mod new file mode 100644 index 0000000..a058946 --- /dev/null +++ b/test/integration/go.mod @@ -0,0 +1,7 @@ +module github.com/foo/bar + +go 1.23.4 + +require github.com/go-chrono/chrono v0.0.0-20240102183611-532f0d0d7c34 // indirect + +replace github.com/go-chrono/chrono => ../../ diff --git a/test/integration/go.sum b/test/integration/go.sum new file mode 100644 index 0000000..90db35d --- /dev/null +++ b/test/integration/go.sum @@ -0,0 +1,2 @@ +github.com/go-chrono/chrono v0.0.0-20240102183611-532f0d0d7c34 h1:eG+4Rhfp++D0gLtwRUktHa71jgFwtsEO3odMM1D8kdU= +github.com/go-chrono/chrono v0.0.0-20240102183611-532f0d0d7c34/go.mod h1:uTWQdzrjtft2vWY+f+KQ9e3DXHsP0SzhE5SLIicFo08= diff --git a/test/integration/main.go b/test/integration/main.go new file mode 100644 index 0000000..87e5795 --- /dev/null +++ b/test/integration/main.go @@ -0,0 +1,12 @@ +// main +package main + +import ( + "fmt" + + "github.com/go-chrono/chrono" +) + +func main() { + fmt.Println(chrono.Now()) +} diff --git a/unsafe.go b/unsafe.go index e6639e8..623afde 100644 --- a/unsafe.go +++ b/unsafe.go @@ -1,8 +1,6 @@ package chrono import ( - "sync" - "time" _ "unsafe" // for go:linkname ) @@ -18,14 +16,5 @@ var zoneSources []string //go:linkname embeddedTzData tzdata.zipdata var embeddedTzData string -//go:linkname readEmbeddedTzData time.loadFromEmbeddedTZData -var readEmbeddedTzData func(zipName string) (string, error) - //go:linkname initLocal time.initLocal func initLocal() - -//go:linkname localLoc time.localLoc -var localLoc time.Location - -//go:linkname localOnce time.localOnce -var localOnce sync.Once