diff options
Diffstat (limited to 'mintages')
-rw-r--r-- | mintages/errors.go | 24 | ||||
-rw-r--r-- | mintages/parser.go | 66 | ||||
-rw-r--r-- | mintages/parser_test.go | 228 |
3 files changed, 280 insertions, 38 deletions
diff --git a/mintages/errors.go b/mintages/errors.go index a456c1a..7319404 100644 --- a/mintages/errors.go +++ b/mintages/errors.go @@ -11,30 +11,6 @@ func (loc location) String() string { return fmt.Sprintf("%s: %d", loc.file, loc.linenr) } -type BadTokenError struct { - location - token string -} - -func (e BadTokenError) Error() string { - return fmt.Sprintf("%s: unknown token ‘%s’", e.location, e.linenr, e.token) -} - -type ArgCountMismatchError struct { - location - token string - expected, got int -} - -func (e ArgCountMismatchError) Error() string { - var suffix string - if e.expected != 1 { - suffix = "s" - } - return fmt.Sprintf("%s: ‘%s’ token expects %d argument%s but got %d", - e.location, e.token, e.expected, suffix, e.got) -} - type SyntaxError struct { location expected, got string diff --git a/mintages/parser.go b/mintages/parser.go index 5c83713..1651115 100644 --- a/mintages/parser.go +++ b/mintages/parser.go @@ -3,6 +3,7 @@ package mintages import ( "bufio" "fmt" + "io" "os" "path/filepath" "strconv" @@ -15,7 +16,7 @@ type coinset [8]int type Data struct { StartYear int - Circ, Bu, Proof []coinset + Circ, BU, Proof []coinset } func ForCountry(code string) (Data, error) { @@ -25,13 +26,16 @@ func ForCountry(code string) (Data, error) { return Data{}, err } defer f.Close() - scanner := bufio.NewScanner(f) + return parse(f, path) +} +func parse(reader io.Reader, path string) (Data, error) { var ( data Data // Our data struct slice *[]coinset // Where to append mintages ) + scanner := bufio.NewScanner(reader) for linenr := 1; scanner.Scan(); linenr++ { line := scanner.Text() tokens := strings.FieldsFunc(strings.TrimSpace(line), unicode.IsSpace) @@ -40,11 +44,10 @@ func ForCountry(code string) (Data, error) { case len(tokens) == 0: continue case tokens[0] == "BEGIN": - if len(tokens) != 2 { - return Data{}, ArgCountMismatchError{ - token: tokens[0], - expected: 1, - got: len(tokens) - 1, + if len(tokens)-1 != 1 { + return Data{}, SyntaxError{ + expected: "single argument to ‘BEGIN’", + got: fmt.Sprintf("%d arguments", len(tokens)-1), location: location{path, linenr}, } } @@ -55,7 +58,7 @@ func ForCountry(code string) (Data, error) { case "CIRC": slice = &data.Circ case "BU": - slice = &data.Bu + slice = &data.BU case "PROOF": slice = &data.Proof default: @@ -68,26 +71,50 @@ func ForCountry(code string) (Data, error) { } data.StartYear, _ = strconv.Atoi(arg) } - case isNumeric(tokens[0], true): + case isNumeric(tokens[0], true), tokens[0] == "?": + switch { + case slice == nil: + return Data{}, SyntaxError{ + expected: "coin type declaration", + got: tokens[0], + location: location{path, linenr}, + } + case data.StartYear == 0: + return Data{}, SyntaxError{ + expected: "start year declaration", + got: tokens[0], + location: location{path, linenr}, + } + } + numcoins := len(coinset{}) tokcnt := len(tokens) if tokcnt != numcoins { + word := "entries" + if tokcnt == 1 { + word = "entry" + } return Data{}, SyntaxError{ expected: fmt.Sprintf("%d mintage entries", numcoins), - got: fmt.Sprintf("%d entries", tokcnt), + got: fmt.Sprintf("%d %s", tokcnt, word), location: location{path, linenr}, } } var row coinset for i, tok := range tokens { - row[i], _ = strconv.Atoi(strings.ReplaceAll(tok, ".", "")) + if tok == "?" { + row[i] = -1 + } else { + row[i] = atoiWithDots(tok) + } } *slice = append(*slice, row) default: - return Data{}, BadTokenError{ - token: tokens[0], + return Data{}, SyntaxError{ + expected: "‘BEGIN’ directive or mintage row", + got: fmt.Sprintf("invalid token ‘%s’", tokens[0]), location: location{path, linenr}, } } @@ -97,7 +124,7 @@ func ForCountry(code string) (Data, error) { for each year that we haven’t filled in info for. This avoids things accidentally breaking if the new year comes and we forget to add extra rows. */ - for _, ms := range [...]*[]coinset{&data.Circ, &data.Bu, &data.Proof} { + for _, ms := range [...]*[]coinset{&data.Circ, &data.BU, &data.Proof} { finalYear := len(*ms) + data.StartYear - 1 missing := time.Now().Year() - finalYear for i := 0; i < missing; i++ { @@ -122,3 +149,14 @@ func isNumeric(s string, dot bool) bool { } return true } + +func atoiWithDots(s string) int { + n := 0 + for _, ch := range s { + if ch == '.' { + continue + } + n = n*10 + int(ch) - '0' + } + return n +} diff --git a/mintages/parser_test.go b/mintages/parser_test.go new file mode 100644 index 0000000..ee2b06e --- /dev/null +++ b/mintages/parser_test.go @@ -0,0 +1,228 @@ +package mintages + +import ( + "bytes" + "errors" + "testing" + "time" +) + +func TestParserComplete(t *testing.T) { + data, err := parse(bytes.NewBuffer([]byte(` + BEGIN 2020 + BEGIN CIRC + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + if err != nil { + t.Fatalf(`Expected err=nil; got "%s"`, err) + } + if data.StartYear != 2020 { + t.Fatalf("Expected data.StartYear=2020; got %d", + data.StartYear) + } + + /* The following 3 loops assert that we have correct mintages, + including padding mintages. After the loops we assert that the + number of padding mintages is actually correct. */ + + for i, row := range data.Circ { + for j, col := range row { + var n int + switch { + case i == 1 && j == 1, i >= 2: + n = -1 + default: + n = 1000*i + j + 1000 + } + if col != n { + t.Fatalf("Expected data.Circ[i][j]=%d; got %d", n, col) + } + } + } + + for i, row := range data.BU { + for j, col := range row { + var n int + switch { + case i == 1 && j == 1, i >= 2: + n = -1 + default: + n = 1000*i + j + 1100 + } + if col != n { + t.Fatalf("Expected data.BU[i][j]=%d; got %d", n, col) + } + } + } + + for i, row := range data.Proof { + for j, col := range row { + var n int + switch { + case i == 1 && j == 1, i >= 2: + n = -1 + default: + n = 1000*i + j + 1200 + } + if col != n { + t.Fatalf("Expected data.Proof[i][j]=%d; got %d", n, col) + } + } + } + + rowsWant := time.Now().Year() - data.StartYear + 1 + if len(data.Circ) != rowsWant { + t.Fatalf("Expected len(data.Circ)=%d; got %d", rowsWant, len(data.Circ)) + } + if len(data.BU) != rowsWant { + t.Fatalf("Expected len(data.BU)=%d; got %d", rowsWant, len(data.BU)) + } + if len(data.Proof) != rowsWant { + t.Fatalf("Expected len(data.Proof)=%d; got %d", rowsWant, len(data.Proof)) + } +} + +func TestParserNoYear(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + BEGIN CIRC + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} + +func TestParserNoType(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + BEGIN 2020 + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} + +func TestParserNoYearOrType(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} + +func TestParserBadToken(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + BEGIN 2020 + BEGIN CIRC + 1.000 1001 1002 1003 1004 1005 1006 1007 + I’m bad + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} + +func TestParserShortRow(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + BEGIN 2020 + BEGIN CIRC + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} + +func TestParserLongRow(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + BEGIN 2020 + BEGIN CIRC + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 2108 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} + +func TestParserBadCoinType(t *testing.T) { + _, err := parse(bytes.NewBuffer([]byte(` + BEGIN 2020 + BEGIN CIRCULATED + 1.000 1001 1002 1003 1004 1005 1006 1007 + 2000 ? 2002 2003 2004 2005 2006 2007 + BEGIN BU + 1.100 1101 1102 1103 1104 1105 1106 1107 + 2100 ? 2102 2103 2104 2105 2106 2107 2108 + BEGIN PROOF + 1.200 1201 1202 1203 1204 1205 1206 1207 + 2200 ? 2202 2203 2204 2205 2206 2207 + `)), "-") + + var sErr SyntaxError + if !errors.As(err, &sErr) { + t.Fatalf("Expected err=SyntaxError; got %s", err) + } +} |