summaryrefslogtreecommitdiffhomepage
path: root/src/mintage
diff options
context:
space:
mode:
Diffstat (limited to 'src/mintage')
-rw-r--r--src/mintage/parser.go297
-rw-r--r--src/mintage/parser_test.go233
2 files changed, 530 insertions, 0 deletions
diff --git a/src/mintage/parser.go b/src/mintage/parser.go
new file mode 100644
index 0000000..364b6e8
--- /dev/null
+++ b/src/mintage/parser.go
@@ -0,0 +1,297 @@
+package mintage
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type SyntaxError struct {
+ expected, got string
+ file string
+ linenr int
+}
+
+func (e SyntaxError) Error() string {
+ return fmt.Sprintf("%s:%d: syntax error: expected %s but got %s",
+ e.file, e.linenr, e.expected, e.got)
+}
+
+type Data struct {
+ Standard []SRow
+ Commemorative []CRow
+}
+
+type SRow struct {
+ Year int
+ Mintmark string
+ Mintages [typeCount][denoms]int
+}
+
+type CRow struct {
+ Year int
+ Mintmark string
+ Name string
+ Mintage [typeCount]int
+}
+
+const (
+ TypeCirc = iota
+ TypeNIFC
+ TypeProof
+ typeCount
+)
+
+const (
+ Unknown = -iota - 1
+ Invalid
+)
+
+const (
+ denoms = 8
+ ws = " \t"
+)
+
+func Parse(r io.Reader, file string) (Data, error) {
+ yearsSince := time.Now().Year() - 1999 + 1
+ data := Data{
+ Standard: make([]SRow, 0, yearsSince),
+ Commemorative: make([]CRow, 0, yearsSince),
+ }
+
+ scanner := bufio.NewScanner(r)
+ for linenr := 1; scanner.Scan(); linenr++ {
+ line := strings.Trim(scanner.Text(), ws)
+ if isBlankOrComment(line) {
+ continue
+ }
+
+ if len(line) < 4 || !isNumeric(line[:4], false) {
+ return Data{}, SyntaxError{
+ expected: "4-digit year",
+ got: line,
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ var (
+ commem bool
+ mintmark string
+ )
+ year, _ := strconv.Atoi(line[:4])
+ line = line[4:]
+
+ if len(line) != 0 {
+ if strings.ContainsRune(ws, rune(line[0])) {
+ commem = true
+ goto out
+ }
+ if line[0] != '-' {
+ return Data{}, SyntaxError{
+ expected: "end-of-line or mintmark",
+ got: line,
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ if line = line[1:]; len(line) == 0 {
+ return Data{}, SyntaxError{
+ expected: "mintmark",
+ got: "end-of-line",
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ switch i := strings.IndexAny(line, ws); i {
+ case 0:
+ return Data{}, SyntaxError{
+ expected: "mintmark",
+ got: "whitespace",
+ linenr: linenr,
+ file: file,
+ }
+ case -1:
+ mintmark = line
+ default:
+ mintmark, line = line[:i], line[i:]
+ commem = true
+ }
+ }
+ out:
+
+ if !commem {
+ row := SRow{
+ Year: year,
+ Mintmark: mintmark,
+ }
+ for i := range row.Mintages {
+ line = ""
+ for isBlankOrComment(line) {
+ if !scanner.Scan() {
+ return Data{}, SyntaxError{
+ expected: "mintage row",
+ got: "end-of-file",
+ linenr: linenr,
+ file: file,
+ }
+ }
+ line = strings.Trim(scanner.Text(), ws)
+ linenr++
+ }
+
+ tokens := strings.FieldsFunc(line, func(r rune) bool {
+ return strings.ContainsRune(ws, r)
+ })
+ if tokcnt := len(tokens); tokcnt != denoms {
+ word := "entries"
+ if tokcnt == 1 {
+ word = "entry"
+ }
+ return Data{}, SyntaxError{
+ expected: fmt.Sprintf("%d mintage entries", denoms),
+ got: fmt.Sprintf("%d %s", tokcnt, word),
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ for j, tok := range tokens {
+ if tok != "?" && !isNumeric(tok, true) {
+ return Data{}, SyntaxError{
+ expected: "numeric mintage figure or ‘?’",
+ got: tok,
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ if tok == "?" {
+ row.Mintages[i][j] = Unknown
+ } else {
+ row.Mintages[i][j] = atoiWithDots(tok)
+ }
+ }
+ }
+
+ data.Standard = append(data.Standard, row)
+ } else {
+ row := CRow{
+ Year: year,
+ Mintmark: mintmark,
+ }
+ line = strings.TrimLeft(line, ws)
+ if line[0] != '"' {
+ return Data{}, SyntaxError{
+ expected: "string",
+ got: line,
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ line = line[1:]
+ switch i := strings.IndexByte(line, '"'); i {
+ case -1:
+ return Data{}, SyntaxError{
+ expected: "closing quote",
+ got: "end-of-line",
+ linenr: linenr,
+ file: file,
+ }
+ case 0:
+ return Data{}, SyntaxError{
+ expected: "commemorated event",
+ got: "empty string",
+ linenr: linenr,
+ file: file,
+ }
+ default:
+ row.Name, line = line[:i], line[i+1:]
+ }
+
+ if len(line) != 0 {
+ return Data{}, SyntaxError{
+ expected: "end-of-line",
+ got: line,
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ for isBlankOrComment(line) {
+ if !scanner.Scan() {
+ return Data{}, SyntaxError{
+ expected: "mintage row",
+ got: "end-of-file",
+ linenr: linenr,
+ file: file,
+ }
+ }
+ line = strings.Trim(scanner.Text(), ws)
+ linenr++
+ }
+
+ tokens := strings.FieldsFunc(line, func(r rune) bool {
+ return strings.ContainsRune(ws, r)
+ })
+ if tokcnt := len(tokens); tokcnt != typeCount {
+ word := "entries"
+ if tokcnt == 1 {
+ word = "entry"
+ }
+ return Data{}, SyntaxError{
+ expected: fmt.Sprintf("%d mintage entries", typeCount),
+ got: fmt.Sprintf("%d %s", tokcnt, word),
+ linenr: linenr,
+ file: file,
+ }
+ }
+
+ for i, tok := range tokens {
+ if tok == "?" {
+ row.Mintage[i] = Unknown
+ } else {
+ row.Mintage[i] = atoiWithDots(tok)
+ }
+ }
+
+ data.Commemorative = append(data.Commemorative, row)
+ }
+ }
+
+ return data, nil
+}
+
+func isNumeric(s string, dot bool) bool {
+ for _, ch := range s {
+ switch ch {
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ default:
+ if ch != '.' || !dot {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func atoiWithDots(s string) int {
+ n := 0
+ for _, ch := range s {
+ if ch == '.' {
+ continue
+ }
+ n = n*10 + int(ch) - '0'
+ }
+ return n
+}
+
+func isBlankOrComment(s string) bool {
+ return len(s) == 0 || s[0] == '#'
+}
diff --git a/src/mintage/parser_test.go b/src/mintage/parser_test.go
new file mode 100644
index 0000000..76e0f01
--- /dev/null
+++ b/src/mintage/parser_test.go
@@ -0,0 +1,233 @@
+package mintage
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+)
+
+func TestParserComplete(t *testing.T) {
+ data, err := Parse(bytes.NewBuffer([]byte(`
+ 2020
+ 1000 1001 1002 1003 1004 1005 1006 1007
+ 1100 1101 1102 1103 1104 1105 1106 1107
+ 1200 1201 1202 1203 1204 1205 1206 1207
+ 2021-KNM
+ 2.000 ? 2002 2003 2004 2005 2006 2007
+ 2.100 ? 2102 2103 2104 2105 2106 2107
+ 2.200 ? 2202 2203 2204 2205 2206 2207
+ 2021-MdP
+ 3000 3001 3002 3003 3004 3005 3006 3007
+ 3100 3101 3102 3103 3104 3105 3106 3107
+ 3200 3201 3202 3203 3204 3205 3206 3207
+ 2022
+ 4000 4001 4.002 4003 4004 4005 4006 4007
+ 4100 4101 4.102 4103 4104 4105 4106 4107
+ 4200 4201 4.202 4203 4204 4205 4206 4207
+
+ 2009 "10th Anniversary of Economic and Monetary Union"
+ 1000 2000 3000
+ 2022-⋆ "35th Anniversary of the Erasmus Programme"
+ 1001 ? 3001
+ `)), "-")
+
+ if err != nil {
+ t.Fatalf(`Expected err=nil; got "%s"`, err)
+ }
+
+ for i, row := range data.Standard {
+ for k := TypeCirc; k <= TypeProof; k++ {
+ for j, col := range row.Mintages[k] {
+ n := 1000*(i+1) + 100*k + j
+ if i == 1 && j == 1 {
+ n = Unknown
+ }
+ if col != n {
+ t.Fatalf("Expected data.Standard[%d].Mintages[%d][%d]=%d; got %d",
+ i, k, j, col, n)
+ }
+ }
+ }
+ }
+
+ for i, row := range data.Commemorative {
+ for k := TypeCirc; k <= TypeProof; k++ {
+ n := 1000*(k+1) + i
+ if i == 1 && k == 1 {
+ n = Unknown
+ }
+ if row.Mintage[k] != n {
+ t.Fatalf("Expected row.Mintage[%d]=%d; got %d",
+ k, n, row.Mintage[k])
+ }
+ }
+ }
+
+ if len(data.Standard) != 4 {
+ t.Fatalf("Expected len(data.Standard)=2; got %d", len(data.Standard))
+ }
+ if len(data.Commemorative) != 2 {
+ t.Fatalf("Expected len(data.Commemorative)=2; got %d", len(data.Commemorative))
+ }
+
+ for i, x := range [...]struct {
+ year int
+ mintmark, name string
+ }{
+ {2009, "", "10th Anniversary of Economic and Monetary Union"},
+ {2022, "⋆", "35th Anniversary of the Erasmus Programme"},
+ } {
+ if data.Commemorative[i].Year != x.year {
+ t.Fatalf("Expected data.Commemorative[%d].Year=%d; got %d",
+ i, x.year, data.Commemorative[i].Year)
+ }
+ if data.Commemorative[i].Mintmark != x.mintmark {
+ t.Fatalf(`Expected data.Commemorative[%d].Mintmark="%s"; got "%s"`,
+ i, x.mintmark, data.Commemorative[i].Mintmark)
+ }
+ if data.Commemorative[i].Name != x.name {
+ t.Fatalf(`Expected data.Commemorative[%d].Name="%s"; got "%s"`,
+ i, x.name, data.Commemorative[i].Name)
+ }
+ }
+}
+
+func TestParserMintmarks(t *testing.T) {
+ data, err := Parse(bytes.NewBuffer([]byte(`
+ 2020
+ 1000 1001 1002 1003 1004 1005 1006 1007
+ 1100 1101 1102 1103 1104 1105 1106 1107
+ 1200 1201 1202 1203 1204 1205 1206 1207
+ 2021-KNM
+ 2.000 ? 2002 2003 2004 2005 2006 2007
+ 2.100 ? 2102 2103 2104 2105 2106 2107
+ 2.200 ? 2202 2203 2204 2205 2206 2207
+ 2021-MdP
+ 3000 3001 3002 3003 3004 3005 3006 3007
+ 3100 3101 3102 3103 3104 3105 3106 3107
+ 3200 3201 3202 3203 3204 3205 3206 3207
+ 2022
+ 4000 4001 4.002 4003 4004 4005 4006 4007
+ 4100 4101 4.102 4103 4104 4105 4106 4107
+ 4200 4201 4.202 4203 4204 4205 4206 4207
+ `)), "-")
+
+ if err != nil {
+ t.Fatalf(`Expected err=nil; got "%s"`, err)
+ }
+
+ for i, row := range data.Standard {
+ for j, col := range row.Mintages[TypeCirc] {
+ n := 1000*(i+1) + j
+ if i == 1 && j == 1 {
+ n = Unknown
+ }
+ if col != n {
+ t.Fatalf("Expected data.Standard[%d].Mintages[TypeCirc][%d]=%d; got %d",
+ i, j, col, n)
+ }
+ }
+ }
+
+ for i, y := range [...]int{2020, 2021, 2021, 2022} {
+ if data.Standard[i].Year != y {
+ t.Fatalf("Expected data.Standard[%d].Year=%d; got %d",
+ i, y, data.Standard[i].Year)
+ }
+ }
+
+ for i, m := range [...]string{"", "KNM", "MdP", ""} {
+ if data.Standard[i].Mintmark != m {
+ t.Fatalf(`Expected data.Standard[%d].Mintmark="%s"; got "%s"`,
+ i, m, data.Standard[i].Mintmark)
+ }
+ }
+}
+
+func TestParserNoYear(t *testing.T) {
+ _, err := Parse(bytes.NewBuffer([]byte(`
+ 1.000 1001 1002 1003 1004 1005 1006 1007
+ 1.100 1101 1102 1103 1104 1105 1106 1107
+ 1.200 1201 1202 1203 1204 1205 1206 1207
+ 2021
+ 2000 ? 2002 2003 2004 2005 2006 2007
+ 2100 ? 2102 2103 2104 2105 2106 2107
+ 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(`
+ 2020
+ 1.000 1001 1002 1003 1004 1005 1006 1007
+ 1.100 1101 1102 1103 1104 1105 1106 1107
+ 1.200 1201 1202 1203 1204 1205 1206 1207
+ 2021 Naughty!
+ 2000 ? 2002 2003 2004 2005 2006 2007
+ 2100 ? 2102 2103 2104 2105 2106 2107
+ 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(`
+ 2020
+ 1.000 1001 1002 1003 1004 1005 1006 1007
+ 1.100 1101 1102 1103 1104 1105 1106 1107
+ 1.200 1201 1202 1203 1204 1205 1206 1207
+ 2021
+ 2000 ? 2002 2003 2004 2005 2006 2007
+ 2100 ? 2102 2103 2104 2105 2106
+ 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(`
+ 2020
+ 1.000 1001 1002 1003 1004 1005 1006 1007
+ 1.100 1101 1102 1103 1104 1105 1106 1107
+ 1.200 1201 1202 1203 1204 1205 1206 1207
+ 2021
+ 2000 ? 2002 2003 2004 2005 2006 2007
+ 2100 ? 2102 2103 2104 2105 2106 2107 2108
+ 2200 ? 2202 2203 2204 2205 2206 2207
+ `)), "-")
+
+ var sErr SyntaxError
+ if !errors.As(err, &sErr) {
+ t.Fatalf("Expected err=SyntaxError; got %s", err)
+ }
+}
+
+func TestParserMissingRow(t *testing.T) {
+ _, err := Parse(bytes.NewBuffer([]byte(`
+ 2020
+ 1.000 1001 1002 1003 1004 1005 1006 1007
+ 1.100 1101 1102 1103 1104 1105 1106 1107
+ 1.200 1201 1202 1203 1204 1205 1206 1207
+ 2021
+ 2000 ? 2002 2003 2004 2005 2006 2007
+ 2200 ? 2202 2203 2204 2205 2206 2207
+ `)), "-")
+
+ var sErr SyntaxError
+ if !errors.As(err, &sErr) {
+ t.Fatalf("Expected err=SyntaxError; got %s", err)
+ }
+}