package jsonx import ( "database/sql/driver" "encoding/json" "fmt" "strconv" "time" "github.com/kataras/iris/v12/x/timex" ) // SimpleDateLayout represents the "year-month-day" Go time format. const SimpleDateLayout = "2006-01-02" // SimpleDate holds a json "year-month-day" time. type SimpleDate time.Time // SimpleDateFromTime accepts a "t" Time and returns // a SimpleDate. If format fails, it returns the zero value of time.Time. func SimpleDateFromTime(t time.Time) SimpleDate { date, _ := ParseSimpleDate(t.Format(SimpleDateLayout)) return date } // ParseSimpleDate reads from "s" and returns the SimpleDate time. func ParseSimpleDate(s string) (SimpleDate, error) { if s == "" || s == "null" { return SimpleDate{}, nil } var ( tt time.Time err error ) tt, err = time.Parse(SimpleDateLayout, s) if err != nil { return SimpleDate{}, err } return SimpleDate(tt.UTC()), nil } // UnmarshalJSON binds the json "data" to "t" with the `SimpleDateLayout`. func (t *SimpleDate) UnmarshalJSON(data []byte) error { if isNull(data) { return nil } data = trimQuotes(data) if len(data) == 0 { return nil // as an excepption here, allow empty "" on simple dates, as the server would render it on a response. } dataStr := string(data) tt, err := time.Parse(SimpleDateLayout, dataStr) if err != nil { return err } *t = SimpleDate(tt) return nil } // MarshalJSON returns the json representation of the "t". func (t SimpleDate) MarshalJSON() ([]byte, error) { if s := t.String(); s != "" { s = strconv.Quote(s) return []byte(s), nil } return emptyQuoteBytes, nil } // IsZero reports whether "t" is zero time. // It completes the pg.Zeroer interface. func (t SimpleDate) IsZero() bool { return t.ToTime().IsZero() } // Add returns the date of "t" plus "d". func (t SimpleDate) Add(d time.Duration) SimpleDate { return SimpleDateFromTime(t.ToTime().Add(d)) } func (t SimpleDate) CountPastDays(pastDate SimpleDate) int { t1, t2 := t.ToTime(), pastDate.ToTime() return int(t1.Sub(t2).Hours() / 24) } // Equal reports back if "t" and "d" equals to the same date. func (t SimpleDate) Equal(d SimpleDate) bool { return t.String() == d.String() } // After reports whether the time instant t is after u. func (t SimpleDate) After(d2 SimpleDate) bool { t1, t2 := t.ToTime(), d2.ToTime() return t1.Truncate(24 * time.Hour).After(t2.Truncate(24 * time.Hour)) } // Before reports whether the time instant t is before u. func (t SimpleDate) Before(d2 SimpleDate) bool { t1, t2 := t.ToTime(), d2.ToTime() return t1.Truncate(24 * time.Hour).Before(t2.Truncate(24 * time.Hour)) // OR: compare year and year's day. } // ToTime returns the standard time type. func (t SimpleDate) ToTime() time.Time { return time.Time(t) } func (t SimpleDate) Value() (driver.Value, error) { return t.String(), nil } // String returns the text representation of the date // formatted based on the `SimpleDateLayout`. // If date is zero it returns an empty string. func (t SimpleDate) String() string { tt := t.ToTime() if tt.IsZero() { return "" } return tt.Format(SimpleDateLayout) } // Scan completes the pg and native sql driver.Scanner interface // reading functionality of a custom type. func (t *SimpleDate) Scan(src interface{}) error { switch v := src.(type) { case time.Time: // type was set to timestamp if v.IsZero() { return nil // don't set zero, ignore it. } *t = SimpleDate(v) case string: tt, err := ParseSimpleDate(v) if err != nil { return err } *t = tt case nil: *t = SimpleDate(time.Time{}) default: return fmt.Errorf("SimpleDate: unknown type of: %T", v) } return nil } // Slice of SimpleDate. type SimpleDates []SimpleDate // First returns the first element of the date slice. func (t SimpleDates) First() SimpleDate { if len(t) == 0 { return SimpleDate{} } return t[0] } // Last returns the last element of the date slice. func (t SimpleDates) Last() SimpleDate { if len(t) == 0 { return SimpleDate{} } return t[len(t)-1] } // DateStrings returns a slice of string representation of the dates. func (t SimpleDates) DateStrings() []string { list := make([]string, 0, len(t)) for _, d := range t { list = append(list, d.String()) } return list } // Scan completes the pg and native sql driver.Scanner interface. func (t *SimpleDates) Scan(src interface{}) error { if src == nil { return nil } var data []byte switch v := src.(type) { case []byte: data = v case string: data = []byte(v) default: return fmt.Errorf("simple dates: scan: invalid type of: %T", src) } err := json.Unmarshal(data, t) return err } // Value completes the pg and native sql driver.Valuer interface. func (t SimpleDates) Value() (driver.Value, error) { if len(t) == 0 { return nil, nil } b, err := json.Marshal(t) return b, err } // Contains reports if the "date" exists inside "t". func (t SimpleDates) Contains(date SimpleDate) bool { for _, v := range t { if v.Equal(date) { return true } } return false } // DateRangeType is the type of the date range. type DateRangeType string const ( // DayRange is the date range type of a day. DayRange DateRangeType = "day" // MonthRange is the date range type of a month. MonthRange DateRangeType = "month" // WeekRange is the date range type of a week. WeekRange DateRangeType = "week" // YearRange is the date range type of a year. YearRange DateRangeType = "year" ) // GetSimpleDateRange returns a slice of SimpleDate between "start" and "end" pf "date" // based on given "typ" (WeekRange, MonthRange...). // // Example Code: // date := jsonx.SimpleDateFromTime(time.Now()) // dates := jsonx.GetSimpleDateRange(date, jsonx.WeekRange, time.Monday, time.Sunday) func GetSimpleDateRange(date SimpleDate, typ DateRangeType, startWeekday, endWeekday time.Weekday) SimpleDates { var dates []time.Time switch typ { case WeekRange: dates = timex.GetWeekdays(date.ToTime(), startWeekday, endWeekday) case MonthRange: dates = timex.GetMonthDays(date.ToTime()) default: panic(fmt.Sprintf("invalid DateRangeType given: %s", typ)) } simpleDates := make(SimpleDates, len(dates)) for i, t := range dates { simpleDates[i] = SimpleDateFromTime(t) } return simpleDates }