Skip to content

Commit

Permalink
- adds support for months in duration (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
baywet authored Jan 26, 2022
1 parent f98819f commit 3410b58
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 16 deletions.
58 changes: 43 additions & 15 deletions duration/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ var (
// ErrBadFormat is returned when parsing fails
ErrBadFormat = errors.New("bad format string")

// ErrNoMonth is raised when a month is in the format string
ErrNoMonth = errors.New("no months allowed")
ErrWeeksNotWithYearsOrMonth = errors.New("weeks are not allowed with years or months")

tmpl = template.Must(template.New("duration").Parse(`P{{if .Years}}{{.Years}}Y{{end}}{{if .Weeks}}{{.Weeks}}W{{end}}{{if .Days}}{{.Days}}D{{end}}{{if .HasTimePart}}T{{end }}{{if .Hours}}{{.Hours}}H{{end}}{{if .Minutes}}{{.Minutes}}M{{end}}{{if .Seconds}}{{.Seconds}}S{{end}}`))
ErrMonthsInDurationUseOverload = errors.New("months are not allowed with the ToDuration method, use the overload instead")

tmpl = template.Must(template.New("duration").Parse(`P{{if .Years}}{{.Years}}Y{{end}}{{if .Months}}{{.Months}}M{{end}}{{if .Weeks}}{{.Weeks}}W{{end}}{{if .Days}}{{.Days}}D{{end}}{{if .HasTimePart}}T{{end }}{{if .Hours}}{{.Hours}}H{{end}}{{if .Minutes}}{{.Minutes}}M{{end}}{{if .Seconds}}{{.Seconds}}S{{end}}`))

full = regexp.MustCompile(`P((?P<year>\d+)Y)?((?P<month>\d+)M)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+(?:\.\d+))S)?)?`)
week = regexp.MustCompile(`P((?P<week>\d+)W)`)
)

type Duration struct {
Years int
Months int
Weeks int
Days int
Hours int
Expand Down Expand Up @@ -67,7 +69,7 @@ func FromString(dur string) (*Duration, error) {
case "year":
d.Years = int(val)
case "month":
return nil, ErrNoMonth
d.Months = int(val)
case "week":
d.Weeks = int(val)
case "day":
Expand All @@ -90,15 +92,17 @@ func FromString(dur string) (*Duration, error) {
return d, nil
}

// String prints out the value passed in. It's not strictly according to the
// ISO spec, but it's pretty close. It would need to disallow weeks mingling with
// other units.
// String prints out the value passed in.
func (d *Duration) String() string {
var s bytes.Buffer

d.Normalize()
err := d.Normalize()

if err != nil {
panic(err)
}

err := tmpl.Execute(&s, d)
err = tmpl.Execute(&s, d)
if err != nil {
panic(err)
}
Expand All @@ -109,12 +113,14 @@ func (d *Duration) String() string {
// Normalize makes sure that all fields are represented as the smallest meaningful value possible by dividing them out by the conversion factor to the larger unit.
// e.g. if you have a duration of 10 day, 25 hour, and 61 minute, it will be normalized to 1 week 5 days, 2 hours, and 1 minute.
// this function does not normalize days to months, weeks to months or weeks to years as they do not always convert with the same value.
func (d *Duration) Normalize() {
// it also won't normalize days to weeks if months or years are present, and will return an error if the value is invalid
func (d *Duration) Normalize() error {
msToS := 1000
StoM := 60
MtoH := 60
HtoD := 24
DtoW := 7
MtoY := 12
if d.MilliSeconds >= msToS {
d.Seconds += d.MilliSeconds / msToS
d.MilliSeconds %= msToS
Expand All @@ -131,11 +137,20 @@ func (d *Duration) Normalize() {
d.Days += d.Hours / HtoD
d.Hours %= HtoD
}
if d.Days >= DtoW {
if d.Days >= DtoW && d.Months == 0 && d.Years == 0 {
d.Weeks += d.Days / DtoW
d.Days %= DtoW
}
//TODO convert 12 months to 1 year when month is supported
if d.Months > MtoY {
d.Years += d.Months / MtoY
d.Months %= MtoY
}

if d.Weeks != 0 && (d.Years != 0 || d.Months != 0) {
return ErrWeeksNotWithYearsOrMonth
}

return nil
// a month is not always 30 days, so we don't normalize that
// a month is not always 4 weeks, so we don't normalize that
// a year is not always 52 weeks, so we don't normalize that
Expand All @@ -145,19 +160,32 @@ func (d *Duration) HasTimePart() bool {
return d.Hours != 0 || d.Minutes != 0 || d.Seconds != 0
}

func (d *Duration) ToDuration() time.Duration {
func (d *Duration) ToDuration() (time.Duration, error) {
if d.Months != 0 {
return 0, ErrMonthsInDurationUseOverload
}
return d.ToDurationWithMonths(31)
}

func (d *Duration) ToDurationWithMonths(daysInAMonth int) (time.Duration, error) {
day := time.Hour * 24
year := day * 365
month := day * time.Duration(daysInAMonth)

tot := time.Duration(0)

d.Normalize()
err := d.Normalize()
if err != nil {
return tot, err
}

tot += year * time.Duration(d.Years)
tot += month * time.Duration(d.Months)
tot += day * 7 * time.Duration(d.Weeks)
tot += day * time.Duration(d.Days)
tot += time.Hour * time.Duration(d.Hours)
tot += time.Minute * time.Duration(d.Minutes)
tot += time.Second * time.Duration(d.Seconds)

return tot
return tot, nil
}
97 changes: 96 additions & 1 deletion duration/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package duration

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -36,7 +37,7 @@ func TestItNormalizesS(t *testing.T) {
assert.Equal(t, "PT1M1S", result)
}

func TestItNormalizesM(t *testing.T) {
func TestItNormalizesMi(t *testing.T) {
// Arrange
duration := &Duration{
Minutes: 61,
Expand Down Expand Up @@ -95,3 +96,97 @@ func TestItDoesntNormalizesW(t *testing.T) {
assert.Equal(t, 0, duration.Years)
assert.Equal(t, "P56W", result)
}

func TestItDoesntNormalizesDWithMo(t *testing.T) {
// Arrange
duration := &Duration{
Days: 15,
Months: 2,
}

// Act
result := duration.String()

// Assert
assert.Equal(t, 15, duration.Days)
assert.Equal(t, 0, duration.Weeks)
assert.Equal(t, 2, duration.Months)
assert.Equal(t, "P2M15D", result)
}

func TestItNormalizesMo(t *testing.T) {
// Arrange
duration := &Duration{
Months: 13,
}

// Act
result := duration.String()

// Assert
assert.Equal(t, 1, duration.Months)
assert.Equal(t, 1, duration.Years)
assert.Equal(t, "P1Y1M", result)
}

func TestItRefusesMoAndW(t *testing.T) {
// Arrange
duration := &Duration{
Months: 13,
Weeks: 10,
}

// Act
result := duration.Normalize()

// Assert
assert.Equal(t, ErrWeeksNotWithYearsOrMonth, result)
}

func TestItRefusesYAndW(t *testing.T) {
// Arrange
duration := &Duration{
Years: 13,
Weeks: 10,
}

// Act
result := duration.Normalize()

// Assert
assert.Equal(t, ErrWeeksNotWithYearsOrMonth, result)
}

func TestItFailsMoToDuration(t *testing.T) {
// Arrange
duration := &Duration{
Months: 13,
Weeks: 10,
}

// Act
result, err := duration.ToDuration()

// Assert
assert.Equal(t, time.Duration(0), result)
assert.Equal(t, ErrMonthsInDurationUseOverload, err)
}

func TestItParsesMonth(t *testing.T) {
// Act
duration, err := FromString("P1M")

// Assert
assert.Nil(t, err)
assert.Equal(t, 1, duration.Months)
}

func TestItParsesMonthAndMinutes(t *testing.T) {
// Act
duration, err := FromString("P1MT1M")

// Assert
assert.Nil(t, err)
assert.Equal(t, 1, duration.Months)
assert.Equal(t, 1, duration.Minutes)
}

0 comments on commit 3410b58

Please sign in to comment.