Skip to content

Commit

Permalink
Date and Range types and functions
Browse files Browse the repository at this point in the history
  • Loading branch information
aodin committed Nov 5, 2015
1 parent 4cee4c1 commit 31ffcb9
Show file tree
Hide file tree
Showing 5 changed files with 496 additions and 0 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,42 @@
# date
Golang's missing date package, including ranges

Date builds on Golang's `time.Time` package to provide a ISO 8601 Date type

Create a new `Date`:

```go
import "github.com/aodin/date"

func main() {
march1st := date.New(2015, 3, 1)
fmt.Println(march1st) // 2015-03-01
}
```

Parse a date:

```go
Parse("2015-03-01")
```

Range operations:

```go
Range(Today(), Today.AddDays(1)).Intersection(Today())
```

```go
EntireMonth().Union(EntireYear())
```

By default, the `Date` type uses the `time.UTC` location. It can be passed to functions requiring the `time.Time` type using the embedded `Time` field:

```go
jan1 := time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)
jan1.Before(march1.Time)
```

Happy Hacking!

aodin, 2015
107 changes: 107 additions & 0 deletions date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package date

import (
"bytes"
"database/sql/driver"
"encoding/json"
"time"
)

// DateLayout uses ISO 8601 as a default for parsing and rendering
const DateLayout = "2006-01-02"

type Date struct{ time.Time }

func (date Date) format() string {
return date.Time.Format(DateLayout)
}

// AddDays adds the given number of days to the date
func (date Date) AddDays(days int) Date {
return Date{Time: date.Time.AddDate(0, 0, days)}
}

func (date Date) After(other Date) bool {
return date.Time.After(other.Time)
}

func (date Date) Before(other Date) bool {
return date.Time.Before(other.Time)
}

// String returns the Date as a string
func (date Date) String() string {
return date.format()
}

// Equals returns true if the dates are equal
func (date Date) Equals(other Date) bool {
return date.Time.Equal(other.Time)
}

// UnmarshalJSON converts a byte array into a Date
func (d *Date) UnmarshalJSON(text []byte) error {
b := bytes.NewBuffer(text)
dec := json.NewDecoder(b)
var s string
if err := dec.Decode(&s); err != nil {
return err
}
value, err := time.Parse(DateLayout, s)
if err != nil {
return err
}
d.Time = value
return nil
}

// MarshalJSON returns the JSON output of a Date
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(`"` + d.format() + `"`), nil
}

// Scan converts an SQL value into a Date
func (date *Date) Scan(value interface{}) error {
date.Time = value.(time.Time)
return nil
}

// Value returns the date formatted for insert into PostgreSQL
func (date Date) Value() (driver.Value, error) {
return date.format(), nil
}

// Within returns true if the Date is within the range - inclusive
func (date Date) Within(term DateRange) bool {
return !(date.Before(term.Start) || date.After(term.End))
}

// Today converts the local time to a Date
func Today() Date {
return FromTime(time.Now())
}

// FromTime creates a Date from a time.Time
func FromTime(t time.Time) Date {
return New(t.Year(), t.Month(), t.Day())
}

// New creates a new Date
func New(year int, month time.Month, day int) Date {
// Remove all second and nano second information and mark as UTC
return Date{Time: time.Date(year, month, day, 0, 0, 0, 0, time.UTC)}
}

// Parse converts a ISO 8601 date string to a Date, possibly returning an error
func Parse(value string) (Date, error) {
return ParseUsingLayout(DateLayout, value)
}

// ParseUsingLayout calls Parse with a different date layout
func ParseUsingLayout(format, value string) (Date, error) {
t, err := time.Parse(format, value)
if err != nil {
return Date{}, err
}
return Date{Time: t}, nil
}
44 changes: 44 additions & 0 deletions date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package date

import (
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestDate(t *testing.T) {
day := New(2015, 3, 1)
assert.Equal(t, "2015-03-01", day.String())

output, err := json.Marshal(day)
assert.Nil(t, err, "JSON marshaling of dates should not error")
assert.Equal(t, []byte(`"2015-03-01"`), output)

nextDay := New(2015, 3, 2)
assert.True(t, nextDay.Equals(day.AddDays(1)))

parsed, err := Parse("2015-03-01")
assert.Nil(t, err, "Parsing of properly formatted dates should not error")
assert.True(t, parsed.Equals(day))

err = day.UnmarshalJSON([]byte(`"2015-03-01"`))
assert.Nil(
t, err, "UnmarshalJSON of a valid slice of bytes should not error",
)

jan1 := time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)
assert.True(t, jan1.Before(day.Time))
}

func TestDate_Within(t *testing.T) {
march1 := New(2015, 3, 1)

feb := EntireMonth(2015, 2)
march := EntireMonth(2015, 3)

assert.False(t, march1.Within(feb))
assert.Equal(t, march1, march.Start)
assert.True(t, march1.Within(march))
}
Loading

0 comments on commit 31ffcb9

Please sign in to comment.