summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--mintages/errors.go24
-rw-r--r--mintages/parser.go66
-rw-r--r--mintages/parser_test.go228
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)
+ }
+}