diff --git a/cmd/nepcal/cli.go b/cmd/nepcal/cli.go index 05feece..82397fc 100644 --- a/cmd/nepcal/cli.go +++ b/cmd/nepcal/cli.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io" "os" @@ -52,7 +53,7 @@ func (nepcalCli) convADToBS(c *cli.Context) error { ad := gregorian(yy, mm, dd) bs, err := nepcal.FromGregorian(ad) if err != nil { - fmt.Fprintln(os.Stderr, "Please supply a date after 04/14/1943.") + fmt.Fprintln(os.Stderr, "Please ensure the date is between 04-13-1918 and 04-12-2044 A.D.") return cli.Exit("", 1) } @@ -74,7 +75,11 @@ func (nepcalCli) convBSToAD(c *cli.Context) error { d, err := nepcal.Date(yy, nepcal.Month(mm), dd) if err != nil { - fmt.Fprintln(os.Stderr, "Please ensure the date is between 1/1/2000 and 12/30/2095") + if errors.Is(err, nepcal.ErrOutOfBounds) { + fmt.Fprintln(os.Stderr, "Please ensure the date is between 01-01-1975 and 12-30-2100 B.S.") + } else { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + } return cli.Exit("", 1) } diff --git a/cmd/nepcal/main_test.go b/cmd/nepcal/main_test.go index b6c4c06..b81153a 100644 --- a/cmd/nepcal/main_test.go +++ b/cmd/nepcal/main_test.go @@ -73,11 +73,11 @@ func TestParseRawDate(t *testing.T) { {"underflow month", "00-21-1994", -1, -1, -1, false}, {"underflow year", "14-21-199", -1, -1, -1, false}, {"overflow year", "14-21-19900", -1, -1, -1, false}, - {"inconversibe month", "aa-21-1994", -1, -1, -1, false}, - {"inconversibe day", "08-aa-1994", -1, -1, -1, false}, - {"inconversibe year", "08-21-xyz", -1, -1, -1, false}, + {"inconversible month", "aa-21-1994", -1, -1, -1, false}, + {"inconversible day", "08-aa-1994", -1, -1, -1, false}, + {"inconversible year", "08-21-xyz", -1, -1, -1, false}, {"underflow number of elements", "08-21", -1, -1, -1, false}, - {"overflwo number of elements", "08-21-1994-01", -1, -1, -1, false}, + {"overflow number of elements", "08-21-1994-01", -1, -1, -1, false}, } for _, test := range tests { diff --git a/nepcal/nepcal.go b/nepcal/nepcal.go index 2801937..fbcf6f2 100644 --- a/nepcal/nepcal.go +++ b/nepcal/nepcal.go @@ -51,6 +51,12 @@ type raw struct { day int } +// String satisfies the stringer interface for 'raw'. The string format returned +// is intended to be used to establish order between dates. +func (r raw) String() string { + return fmt.Sprintf("%d-%02d-%02d", r.year, r.month, r.day) +} + // Now returns the nepcal.Time struct corresponding to the current time. // This method uses FromGregorianUnchecked - read the documentation // for that method to understand limitations. @@ -93,11 +99,26 @@ func FromGregorianUnchecked(t time.Time) Time { // Date constructs a B.S. date using raw parts "year, month, date". As with the, // "From_" constructors, the specified B.S date must be in the supported range as // specified by the IsInRangeBS function. +// +// Unlike Go's time package, Nepcal does not auto normalize dates, therefore if a date was passed in that +// would have wrapped over, an error is returned. func Date(year int, month Month, day int) (Time, error) { + if month < Baisakh || month > Chaitra { + return Time{}, fmt.Errorf("Month values can only be between 1 and 12, but a value of %d was specified", int(month)) + } + if !IsInRangeBS(year, month, day) { return Time{}, ErrOutOfBounds } + // Check that the provided day actually exists in the month. While Go's time package + // performs auto date normalization (i.e. February 30 is normalized to April 2nd), + // Nepcal does not do this, since this is most likely a user error. + daysInMonth := bsDaysInMonthsByYear[year][int(month)-1] + if day > daysInMonth { + return Time{}, fmt.Errorf(fmt.Sprintf("The month of %s has only %d days in the year %d, but the provided date specifies a value of %d. Nepcal does not perform auto date normalization.", month, daysInMonth, year, day)) + } + inraw := raw{year, month, day} return fromRaw(inraw), nil } @@ -193,7 +214,7 @@ func (t Time) Calendar() io.Reader { // After reports whether the Time t, is after u. func (t Time) After(u Time) bool { - return after(t.toRaw(), u.toRaw()) + return t.toRaw().String() > u.toRaw().String() } // String satisfies the stringer interface. diff --git a/nepcal/nepcal_test.go b/nepcal/nepcal_test.go index bbdf26f..0006aa7 100644 --- a/nepcal/nepcal_test.go +++ b/nepcal/nepcal_test.go @@ -91,7 +91,7 @@ func TestFromGregorian(t *testing.T) { } func TestBsAdConversion(t *testing.T) { - tests := []struct { + validDateTests := []struct { name string input raw output time.Time @@ -138,7 +138,7 @@ func TestBsAdConversion(t *testing.T) { }, } - for _, test := range tests { + for _, test := range validDateTests { t.Run(test.name, func(t *testing.T) { bs, err := Date(test.input.year, test.input.month, test.input.day) @@ -146,11 +146,39 @@ func TestBsAdConversion(t *testing.T) { assert.Equal(t, test.output, bs.in) }) } +} - t.Run("panics if date is before 1975 Baisakh 1", func(t *testing.T) { - _, err := Date(1974, 12, 30) - assert.Equal(t, err, ErrOutOfBounds) - }) +// Test suite that verifies the "Date" function i.e. the BS/AD conversion +// correctly catches various cases of adversarial input. +func TestBsAdConversionAdversarialInput(t *testing.T) { + testCases := []struct { + name string + + // BS input date. + input raw + + // expected error message + err string + }{ + { + "case1/2076_chaitra_only_contains_30_days", + raw{2076, 12, 31}, + "The month of चैत has only 30 days in the year 2076, but the provided date specifies a value of 31. Nepcal does not perform auto date normalization.", + }, + { + "case2/month_does_not_fall_between_baisakh_and_chaitra", + raw{2076, 15, 01}, // 15 is not a valid month + "Month values can only be between 1 and 12, but a value of 15 was specified", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + _, err := Date(test.input.year, test.input.month, test.input.day) + assert.Error(t, err) + assert.Equal(t, test.err, err.Error()) + }) + } } func TestNumDays(t *testing.T) { diff --git a/nepcal/util.go b/nepcal/util.go index d12eb5c..42a5185 100644 --- a/nepcal/util.go +++ b/nepcal/util.go @@ -1,27 +1,37 @@ package nepcal import ( - "fmt" "time" ) -// IsInRangeGregorian checks if 't' is after 04/14/1943. +// IsInRangeGregorian checks if 't' is inside Nepcal's supported range of dates. func IsInRangeGregorian(t time.Time) bool { adLBound := gregorian(adLBoundY, adLBoundM, adLBoundD) + adUBound := gregorian(adUBoundY, adUBoundM, adUBoundD) - return t.Equal(adLBound) || t.After(adLBound) + satisfiesLowerBound := t.Equal(adLBound) || t.After(adLBound) + satisfiesUpperBound := t.Equal(adUBound) || t.Before(adUBound) + + return satisfiesLowerBound && satisfiesUpperBound } // IsInRangeBS checks if the provided date represents a B.S. that // we have data for and can be supported for conversions to/from A.D. func IsInRangeBS(year int, month Month, day int) bool { + if month < Baisakh || month > Chaitra { + return false + } + + t := (raw{year, month, day}).String() + // Lower bound raw date. bslow := raw{bsLBoundY, bsLBoundM, bsLBoundD} + bshigh := raw{bsUBoundY, bsUBoundM, bsUBoundD} - // Input raw date. - inraw := raw{year, month, day} + satisfiesLowerBound := t >= bslow.String() + satisfiesUpperBound := t <= bshigh.String() - return after(inraw, bslow) + return satisfiesLowerBound && satisfiesUpperBound } // IsInRangeYear return true if the provided bsYear is within the supported @@ -48,13 +58,3 @@ func numDaysInYear(year int) int { return sum } - -// Check if 't' is after 'u'. -func after(t raw, u raw) bool { - // Comparing their string representations is an easy way to do this - // as we do not deal with sub-day precisions. - tstr := fmt.Sprintf("%d-%02d-%02d", t.year, t.month, t.day) - ustr := fmt.Sprintf("%d-%02d-%02d", u.year, u.month, u.day) - - return tstr > ustr -}