magnetico/pkg/persistence/iso8601.go
2018-12-30 08:24:33 +03:00

206 lines
4.2 KiB
Go

package persistence
import (
"fmt"
"regexp"
"strconv"
"time"
)
var yearRE = regexp.MustCompile(`^(\d{4})$`)
var monthRE = regexp.MustCompile(`^(\d{4})-(\d{2})$`)
var weekRE = regexp.MustCompile(`^(\d{4})-W(\d{2})$`)
var dayRE = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})$`)
var hourRE = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})T(\d{2})$`)
type Granularity int
const (
Year Granularity = iota
Month
Week
Day
Hour
)
// ParseISO8601 is **not** a function to parse all and every kind of valid ISO 8601
// date, nor it's intended to be, since we don't need that.
func ParseISO8601(s string) (*time.Time, Granularity, error) {
if matches := yearRE.FindStringSubmatch(s); len(matches) != 0 {
year, err := parseYear(matches[1])
if err != nil {
return nil, -1, err
}
t := time.Date(year, time.December, daysOfMonth(time.December, year), 23, 59, 59, 0, time.UTC)
return &t, Year, nil
}
if matches := monthRE.FindStringSubmatch(s); len(matches) != 0 {
month, err := parseMonth(matches[2])
if err != nil {
return nil, -1, err
}
year, err := parseYear(matches[1])
if err != nil {
return nil, -1, err
}
t := time.Date(year, month, 31, 23, 59, 59, 0, time.UTC)
return &t, Month, nil
}
if matches := weekRE.FindStringSubmatch(s); len(matches) != 0 {
week, err := parseWeek(matches[2])
if err != nil {
return nil, -1, err
}
year, err := parseYear(matches[1])
if err != nil {
return nil, -1, err
}
t := time.Date(year, time.January, week*7, 23, 59, 59, 0, time.UTC)
return &t, Week, nil
}
if matches := dayRE.FindStringSubmatch(s); len(matches) != 0 {
month, err := parseMonth(matches[2])
if err != nil {
return nil, -1, err
}
year, err := parseYear(matches[1])
if err != nil {
return nil, -1, err
}
day, err := parseDay(matches[3], daysOfMonth(month, year))
if err != nil {
return nil, -1, err
}
t := time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
return &t, Day, nil
}
if matches := hourRE.FindStringSubmatch(s); len(matches) != 0 {
month, err := parseMonth(matches[2])
if err != nil {
return nil, -1, err
}
year, err := parseYear(matches[1])
if err != nil {
return nil, -1, err
}
hour, err := parseHour(matches[4])
if err != nil {
return nil, -1, err
}
day, err := parseDay(matches[3], daysOfMonth(month, year))
if err != nil {
return nil, -1, err
}
t := time.Date(year, month, day, hour, 59, 59, 0, time.UTC)
return &t, Hour, nil
}
return nil, -1, fmt.Errorf("string does not match any formats")
}
func daysOfMonth(month time.Month, year int) int {
switch month {
case time.January:
return 31
case time.February:
if isLeap(year) {
return 29
} else {
return 28
}
case time.March:
return 31
case time.April:
return 30
case time.May:
return 31
case time.June:
return 30
case time.July:
return 31
case time.August:
return 31
case time.September:
return 30
case time.October:
return 31
case time.November:
return 30
case time.December:
return 31
default:
panic("invalid month!")
}
}
func isLeap(year int) bool {
if year%4 != 0 {
return false
} else if year%100 != 0 {
return true
} else if year%400 != 0 {
return false
} else {
return true
}
}
func atoi(s string) int {
i, e := strconv.Atoi(s)
if e != nil {
// panic on error since atoi() will be called only after we parse it with regex
// (hopefully `\d`!)
panic(e.Error())
}
return i
}
func parseYear(s string) (int, error) {
year := atoi(s)
if year <= 1583 {
return 0, fmt.Errorf("years before 1583 are not allowed")
}
return year, nil
}
func parseMonth(s string) (time.Month, error) {
month := atoi(s)
if month <= 0 || month >= 13 {
return time.Month(-1), fmt.Errorf("month is not in range [01, 12]")
}
return time.Month(month), nil
}
func parseWeek(s string) (int, error) {
week := atoi(s)
if week <= 0 || week >= 54 {
return -1, fmt.Errorf("week is not in range [01, 53]")
}
return week, nil
}
func parseDay(s string, max int) (int, error) {
day := atoi(s)
if day <= 0 || day > max {
return -1, fmt.Errorf("day is not in range [01, %d]", max)
}
return day, nil
}
func parseHour(s string) (int, error) {
hour := atoi(s)
if hour <= -1 || hour >= 25 {
return -1, fmt.Errorf("hour is not in range [00, 24]")
}
return hour, nil
}