Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow normalization and improve robustness against invalid input #80

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions cmd/nepcal/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/nepcal/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 22 additions & 1 deletion nepcal/nepcal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
40 changes: 34 additions & 6 deletions nepcal/nepcal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,19 +138,47 @@ 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)

assert.NoError(t, err)
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) {
Expand Down
32 changes: 16 additions & 16 deletions nepcal/util.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}