date

package module
v1.20240411.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2024 License: Apache-2.0 Imports: 6 Imported by: 0

README

go-date

GoDoc Go ReportCard Build Status codecov

The go-date package provides a dedicated Date{} struct to emulate the standard library time.Time{} behavior.

API

This package provides helpers for:

  • conversion: ToTime(), date.FromTime(), date.FromString()
  • serialization: text, JSON, and SQL
  • emulating time.Time{}: After(), Before(), Sub(), etc.
  • explicit null handling: NullDate{} and an analog of sql.NullTime{}
  • emulating time helpers: Today() as an analog of time.Now()

Background

The Go standard library contains no native type for dates without times. Instead, common convention is to use a time.Time{} with only the year, month, and day set. For example, this convention is followed when a timestamp of the form YYYY-MM-DD is parsed via time.Parse(time.DateOnly, value).

Conversion

For cases where existing code produces a "conventional" time.Date(YYYY, MM, DD, 0, 0, 0, 0, time.UTC) value, it can be validated and converted to a Date{} via:

t := time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC)
d, err := date.FromTime(t)
fmt.Println(d, err)
// 2024-03-01 <nil>

If there is any deviation from the "conventional" format, this will error. For example:

timestamp contains more than just date information; 2020-05-11T01:00:00Z
timestamp contains more than just date information; 2022-01-31T00:00:00-05:00

For cases where we have a discrete timestamp (e.g. "last updated datetime") and a relevant timezone for a given request, we can extract the date within that timezone:

t := time.Date(2023, time.April, 14, 3, 55, 4, 777000100, time.UTC)
tz, _ := time.LoadLocation("America/Chicago")
d := date.InTimezone(t, tz)
fmt.Println(d)
// 2023-04-13

For conversion in the other direction, a Date{} can be converted back into a time.Time{}:

d := date.NewDate(2017, time.July, 3)
t := d.ToTime()
fmt.Println(t)
// 2017-07-03 00:00:00 +0000 UTC

By default this will use the "conventional" format, but any of the values (other than year, month, day) can also be set:

d := date.NewDate(2017, time.July, 3)
tz, _ := time.LoadLocation("America/Chicago")
t := d.ToTime(date.OptConvertHour(12), date.OptConvertTimezone(tz))
fmt.Println(t)
// 2017-07-03 12:00:00 -0500 CDT

Equivalent methods

There are a number of methods from time.Time{} that directly translate over:

d := date.NewDate(2020, time.February, 29)
fmt.Println(d.Year)
// 2020
fmt.Println(d.Month)
// February
fmt.Println(d.Day)
// 29
fmt.Println(d.ISOWeek())
// 2020 9
fmt.Println(d.Weekday())
// Saturday
fmt.Println(d.YearDay())
// 60
fmt.Println(d.Date())
// 2020 February 29

fmt.Println(d.IsZero())
// false
fmt.Println(d.String())
// 2020-02-29
fmt.Println(d.Format("Jan 2006"))
// Feb 2020
fmt.Println(d.GoString())
// date.NewDate(2020, time.February, 29)

d2 := date.NewDate(2021, time.February, 28)
fmt.Println(d2.Equal(d))
// false
fmt.Println(d2.Before(d))
// false
fmt.Println(d2.After(d))
// true
fmt.Println(d2.Compare(d))
// 1

However, some methods translate over only approximately. For example, it's much more natural for Sub() to return the number of days between two dates:

d := date.NewDate(2020, time.February, 29)
d2 := date.NewDate(2021, time.February, 28)
fmt.Println(d2.Sub(d))
// 365

Divergent methods

We've elected to translate the time.Time{}.AddDate() method rather than providing it directly:

d := date.NewDate(2020, time.February, 29)
fmt.Println(d.AddDays(1))
// 2020-03-01
fmt.Println(d.AddDays(100))
// 2020-06-08
fmt.Println(d.AddMonths(1))
// 2020-03-29
fmt.Println(d.AddMonths(3))
// 2020-05-29
fmt.Println(d.AddYears(1))
// 2021-02-28

This is in part because of the behavior of the standard library's AddDate(). In particular, it "overflows" a target month if the number of days in that month is less than the number of desired days. As a result, we provide *Stdlib() variants of the date addition helpers:

d := date.NewDate(2020, time.February, 29)
fmt.Println(d.AddMonths(12))
// 2021-02-28
fmt.Println(d.AddMonthsStdlib(12))
// 2021-03-01
fmt.Println(d.AddYears(1))
// 2021-02-28
fmt.Println(d.AddYearsStdlib(1))
// 2021-03-01

In the same line of thinking as the divergent AddMonths() behavior, a MonthEnd() method is provided that can pinpoint the number of days in the current month:

d := date.NewDate(2022, time.January, 14)
fmt.Println(d.MonthEnd())
// 2022-01-31
fmt.Println(d.MonthStart())
// 2022-01-01

Integrating with sqlc

Out of the box, the sqlc library uses a Go time.Time{} both for columns of type TIMESTAMPTZ and DATE. When reading DATE values (which come over the wire in the form YYYY-MM-DD), the Go standard library produces values of the form:

time.Date(YYYY, MM, DD, 0, 0, 0, 0, time.UTC)

Instead, we can instruct sqlc to globally use date.Date and date.NullDate when parsing DATE columns:

---
version: "2"
overrides:
  go:
    overrides:
      - go_type:
          import: github.com/hardfinhq/go-date
          package: date
          type: NullDate
        db_type: date
        nullable: true
      - go_type:
          import: github.com/hardfinhq/go-date
          package: date
          type: Date
        db_type: date
        nullable: false

Alternatives

This package is intended to be simple to understand and only needs to cover "modern" dates (i.e. dates between 1900 and 2100). As a result, the core Date{} struct directly exposes the year, month, and day as fields.

There are several alternative date packages which cover wider date ranges. (These packages all use the proleptic Gregorian calendar to cover the historical date ranges.) Some existing packages:

Additionally, there is a Date{} type provided by the github.com/jackc/pgtype package that is part of the pgx ecosystem. However, this type is very focused on being useful for database serialization and deserialization and doesn't implement a wider set of methods present on time.Time{} (e.g. After()).

Documentation

Overview

Package date provides tools for working with dates, extending the standard library `time` package.

This package provides helpers for converting from a full `time.Time{}` to a `Date{}` and back, providing validation along the way. Many methods from `time.Time{}` are also provided as equivalents here (`After()`, `Before()`, `Sub()`, etc.). Additionally, custom serialization methods are provided both for JSON and SQL.

The Go standard library contains no native type for dates without times. Instead, common convention is to use a `time.Time{}` with only the year, month, and day set. For example, this convention is followed when a timestamp of the form YYYY-MM-DD is parsed via `time.Parse(time.DateOnly, value)`.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NullTimeFromPtr

func NullTimeFromPtr(d *Date, opts ...ConvertOption) sql.NullTime

NullTimeFromPtr converts a date to a native Go `sql.NullTime`; the convention in Go is that a **date-only** is parsed (via `time.DateOnly`) as `time.Date(YYYY, MM, DD, 0, 0, 0, 0, time.UTC)`.

Types

type ConvertConfig

type ConvertConfig struct {
	Hour       int
	Minute     int
	Second     int
	Nanosecond int
	Timezone   *time.Location
}

ConvertConfig helps customize the behavior of conversion functions like `NullTimeFromPtr()`.

It allows setting the fields in a `time.Time{}` **other** than year, month, and day (i.e. the fields that aren't present in a date). By default, these are: - hour=0 - minute=0 - second=0 - nanosecond=0 - timezone/loc=time.UTC

type ConvertOption

type ConvertOption func(*ConvertConfig)

ConvertOption defines a function that will be applied to a convert config.

func OptConvertHour added in v1.20240228.1

func OptConvertHour(hour int) ConvertOption

OptConvertHour returns an option that sets the hour on a convert config.

func OptConvertMinute added in v1.20240228.1

func OptConvertMinute(minute int) ConvertOption

OptConvertMinute returns an option that sets the minute on a convert config.

func OptConvertNanosecond added in v1.20240228.1

func OptConvertNanosecond(nanosecond int) ConvertOption

OptConvertNanosecond returns an option that sets the nanosecond on a convert config.

func OptConvertSecond added in v1.20240228.1

func OptConvertSecond(second int) ConvertOption

OptConvertSecond returns an option that sets the second on a convert config.

func OptConvertTimezone

func OptConvertTimezone(tz *time.Location) ConvertOption

OptConvertTimezone returns an option that sets the timezone on a convert config.

type Date

type Date struct {
	Year  int
	Month time.Month
	Day   int
}

Date is a simple date (i.e. without timestamp). This is intended to be JSON serialized / deserialized as YYYY-MM-DD.

func FromString

func FromString(s string) (Date, error)

FromString parses a string of the form YYYY-MM-DD into a `Date{}`.

func FromTime

func FromTime(t time.Time) (Date, error)

FromTime validates that a `time.Time{}` contains a date and converts it to a `Date{}`.

func InTimezone

func InTimezone(t time.Time, tz *time.Location) Date

InTimezone translates a timestamp into a timezone and then captures the date in that timezone.

func NewDate

func NewDate(year int, month time.Month, day int) Date

NewDate returns a new `Date` struct. This is a pure convenience function to make it more ergonomic to create a `Date` struct.

func Today

func Today(opts ...TodayOption) Date

Today determines the **current** `Date`, shifted to a given timezone if need be.

Defaults to using UTC and `time.Now()` to determine the current time.

func (Date) AddDays

func (d Date) AddDays(days int) Date

AddDays returns the date corresponding to adding the given number of days.

func (Date) AddMonths

func (d Date) AddMonths(months int) Date

AddMonths returns the date corresponding to adding the given number of months. This accounts for leap years and variable length months. Typically the only change is in the month and year but for changes that would exceed the number of days in the target month, the last day of the month is used.

For example: - adding 1 month to 2020-05-11 results in 2020-06-11 - adding 1 month to 2022-01-31 results in 2022-02-28 - adding 3 months to 2024-01-31 results in 2024-04-30 - subtracting 2 months from 2022-01-31 results in 2022-11-30

NOTE: This behavior is very similar to but distinct from `time.Time{}.AddDate()` specialized to `months` only.

func (Date) AddMonthsStdlib

func (d Date) AddMonthsStdlib(months int) Date

AddMonthsStdlib returns the date corresponding to adding the given number of months, using `time.Time{}.AddDate()` from the standard library. This may "overshoot" if the target date is not a valid date in that month, e.g. 2020-02-31.

For example: - adding 1 month to 2020-05-11 results in 2020-06-11 - adding 1 month to 2022-01-31 results in 2022-03-03 - adding 3 months to 2024-01-31 results in 2024-05-01 - subtracting 2 months from 2022-01-31 results in 2022-12-01

func (Date) AddYears

func (d Date) AddYears(years int) Date

AddYears returns the date corresponding to adding the given number of years, using `time.Time{}.AddDate()` from the standard library. This may "overshoot" if the target date is not a valid date in that month, e.g. 2020-02-31.

For example: - adding 1 year to 2020-02-29 results in 2021-03-01 - adding 1 year to 2023-02-28 results in 2024-02-28 - adding 10 years to 2010-05-01 results in 2020-05-01 - subtracting 10 years from 2010-05-01 results in 2000-05-01

NOTE: This behavior is very similar to but distinct from `time.Time{}.AddDate()` specialized to `years` only.

func (Date) AddYearsStdlib

func (d Date) AddYearsStdlib(years int) Date

AddYearsStdlib returns the date corresponding to adding the given number of years. This accounts for leap years and variable length months. Typically the only change is in the month and year but for changes that would exceed the number of days in the target month, the last day of the month is used.

For example: - adding 1 year to 2020-02-29 results in 2021-02-28 - adding 1 year to 2023-02-28 results in 2024-02-28 - adding 10 years to 2010-05-01 results in 2020-05-01 - subtracting 10 years from 2010-05-01 results in 2000-05-01

NOTE: This behavior is very similar to but distinct from `time.Time{}.AddDate()` specialized to `years` only.

func (Date) After

func (d Date) After(other Date) bool

After returns true if the date is after the other date.

func (Date) Before

func (d Date) Before(other Date) bool

Before returns true if the date is before the other date.

func (Date) Compare added in v1.20240228.1

func (d Date) Compare(other Date) int

Compare compares the date d with other. If d is before other, it returns -1; if d is after other, it returns +1; if they're the same, it returns 0.

func (Date) Date added in v1.20240312.1

func (d Date) Date() (int, time.Month, int)

Date returns the year, month, and day in which `d` occurs.

This is here for parity with `time.Time{}.Date()` and is likely not needed.

func (Date) Equal

func (d Date) Equal(other Date) bool

Equal returns true if the date is equal to the other date.

func (Date) Format

func (d Date) Format(layout string) string

Format returns a textual representation of the date value formatted according to the provided layout. This uses `time.Time{}.Format()` directly and is provided here for convenience.

func (Date) GoString added in v1.20240228.1

func (d Date) GoString() string

GoString implements `fmt.GoStringer`.

func (Date) ISOWeek added in v1.20240228.1

func (d Date) ISOWeek() (year, week int)

ISOWeek returns the ISO 8601 year and week number in which `d` occurs. Week ranges from 1 to 53. Jan 01 to Jan 03 of year `n` might belong to week 52 or 53 of year `n-1`, and Dec 29 to Dec 31 might belong to week 1 of year `n+1`.

func (Date) IsZero

func (d Date) IsZero() bool

IsZero returns true if the date is the zero value.

func (Date) MarshalJSON

func (d Date) MarshalJSON() ([]byte, error)

MarshalJSON implements `json.Marshaler`; formats the date as YYYY-MM-DD.

func (Date) MarshalText added in v1.20240228.1

func (d Date) MarshalText() ([]byte, error)

MarshalText implements the encoding.TextMarshaler interface.

func (Date) MonthEnd

func (d Date) MonthEnd() Date

MonthEnd returns the last date in the month of the current date.

func (Date) MonthStart added in v1.20240228.1

func (d Date) MonthStart() Date

MonthStart returns the first date in the month of the current date.

func (*Date) Scan

func (d *Date) Scan(src any) error

Scan implements `sql.Scanner`; it unmarshals values of the type `time.Time` onto the current `Date` struct.

func (Date) String

func (d Date) String() string

String implements `fmt.Stringer`.

func (Date) Sub

func (d Date) Sub(other Date) int64

Sub returns the number of days `d - other`; this converts both dates to a `time.Time{}` UTC and then dispatches to `time.Time{}.Sub()`.

func (Date) SubErr

func (d Date) SubErr(other Date) (int64, error)

SubErr returns the number of days `d - other`; this converts both dates to a `time.Time{}` UTC and then dispatches to `time.Time{}.Sub()`.

If the number of days is not a whole number (due to overflow), an error is returned.

func (Date) ToTime

func (d Date) ToTime(opts ...ConvertOption) time.Time

ToTime converts the date to a native Go `time.Time`; the convention in Go is that a **date-only** is parsed (via `time.DateOnly`) as `time.Date(YYYY, MM, DD, 0, 0, 0, 0, time.UTC)`.

func (*Date) UnmarshalJSON

func (d *Date) UnmarshalJSON(data []byte) error

UnmarshalJSON implements `json.Unmarshaler`; parses the date as YYYY-MM-DD.

func (*Date) UnmarshalText added in v1.20240228.1

func (d *Date) UnmarshalText(data []byte) error

UnmarshalText implements the encoding.TextUnmarshaler interface. The time must be in the format YYYY-MM-DD.

func (Date) Value

func (d Date) Value() (driver.Value, error)

Value implements `driver.Valuer`; it marshals the value to a `time.Time` to be serialized into the database.

func (Date) Weekday added in v1.20240228.1

func (d Date) Weekday() time.Weekday

Weekday returns the day of the week specified by `d`.

func (Date) YearDay added in v1.20240312.1

func (d Date) YearDay() int

YearDay returns the day of the year specified by `d`, in the range [1,365] for non-leap years, and [1,366] in leap years.

type NullDate

type NullDate struct {
	Date  Date
	Valid bool
}

NullDate is a `Date` that can be null.

func NullDateFromPtr

func NullDateFromPtr(d *Date) NullDate

NullDateFromPtr converts a `Date` pointer into a `NullDate`.

func (*NullDate) Scan

func (nd *NullDate) Scan(value any) error

Scan implements `sql.Scanner`; it unmarshals nullable values of the type `time.Time` onto the current `NullDate` struct.

func (NullDate) Value

func (nd NullDate) Value() (driver.Value, error)

Value implements `driver.Valuer`; it marshals the value to a `time.Time` (or `nil`) to be serialized into the database.

type TodayConfig

type TodayConfig struct {
	Timezone    *time.Location
	NowProvider func() time.Time
}

TodayConfig helps customize the behavior of `Today()`.

type TodayOption

type TodayOption func(*TodayConfig)

TodayOption defines a function that will be applied to a `Today()` config.

func OptTodayNow

func OptTodayNow(now time.Time) TodayOption

OptTodayNow returns an option that sets the now provider on a `Today()` config to return a **constant** `now` value.

This is expected to be used in tests.

func OptTodayTimezone

func OptTodayTimezone(tz *time.Location) TodayOption

OptTodayTimezone returns an option that sets the timezone on a `Today()` config.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL