summaryrefslogtreecommitdiffhomepage
path: root/lib/mintage
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2024-08-11 03:13:10 +0200
committerThomas Voss <mail@thomasvoss.com> 2024-08-11 03:16:36 +0200
commit3273c65ef82123bf5edbe6d8616630b20a993ce1 (patch)
treed230d18f60122f3a1b65056df197ad1db0ca41b0 /lib/mintage
parent3e723305c61673a767da25f683777de368899d41 (diff)
Giant refactoring of the codebase
Diffstat (limited to 'lib/mintage')
-rw-r--r--lib/mintage/parser.go207
-rw-r--r--lib/mintage/parser_test.go280
2 files changed, 487 insertions, 0 deletions
diff --git a/lib/mintage/parser.go b/lib/mintage/parser.go
new file mode 100644
index 0000000..4c5e6f9
--- /dev/null
+++ b/lib/mintage/parser.go
@@ -0,0 +1,207 @@
+package mintage
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+const (
+ _ = -iota
+ Unknown // Unknown mintage
+ Invalid // All mintages <= than this are invalid
+)
+
+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 Row struct {
+ Year int
+ Mintmark string
+ Cols [8]int
+}
+
+type Set struct {
+ StartYear int
+ Circ, BU, Proof []Row
+}
+
+func (r Row) Label() string {
+ if r.Mintmark != "" {
+ return fmt.Sprintf("%d %s", r.Year, r.Mintmark)
+ }
+ return strconv.Itoa(r.Year)
+}
+
+func Parse(reader io.Reader, file string) (Set, error) {
+ var (
+ data Set // Our data struct
+ slice *[]Row // Where to append mintages
+ )
+
+ scanner := bufio.NewScanner(reader)
+ for linenr := 1; scanner.Scan(); linenr++ {
+ var mintmark struct {
+ s string
+ star bool
+ }
+
+ line := scanner.Text()
+ tokens := strings.FieldsFunc(strings.TrimSpace(line), unicode.IsSpace)
+
+ switch {
+ case len(tokens) == 0:
+ continue
+ case tokens[0] == "BEGIN":
+ if len(tokens)-1 != 1 {
+ return Set{}, SyntaxError{
+ expected: "single argument to ‘BEGIN’",
+ got: fmt.Sprintf("%d arguments", len(tokens)-1),
+ file: file,
+ linenr: linenr,
+ }
+ }
+
+ arg := tokens[1]
+
+ switch arg {
+ case "CIRC":
+ slice = &data.Circ
+ case "BU":
+ slice = &data.BU
+ case "PROOF":
+ slice = &data.Proof
+ default:
+ if !isNumeric(arg, false) {
+ return Set{}, SyntaxError{
+ expected: "‘CIRC’, ‘BU’, ‘PROOF’, or a year",
+ got: arg,
+ file: file,
+ linenr: linenr,
+ }
+ }
+ data.StartYear, _ = strconv.Atoi(arg)
+ }
+ case isLabel(tokens[0]):
+ n := len(tokens[0])
+ if n > 2 && tokens[0][n-2] == '*' {
+ mintmark.star = true
+ mintmark.s = tokens[0][:n-2]
+ } else {
+ mintmark.s = tokens[0][:n-1]
+ }
+ tokens = tokens[1:]
+ if !isNumeric(tokens[0], true) && tokens[0] != "?" {
+ return Set{}, SyntaxError{
+ expected: "mintage row after label",
+ got: tokens[0],
+ file: file,
+ linenr: linenr,
+ }
+ }
+ fallthrough
+ case isNumeric(tokens[0], true), tokens[0] == "?":
+ switch {
+ case slice == nil:
+ return Set{}, SyntaxError{
+ expected: "coin type declaration",
+ got: tokens[0],
+ file: file,
+ linenr: linenr,
+ }
+ case data.StartYear == 0:
+ return Set{}, SyntaxError{
+ expected: "start year declaration",
+ got: tokens[0],
+ file: file,
+ linenr: linenr,
+ }
+ }
+
+ numcoins := len(Row{}.Cols)
+ tokcnt := len(tokens)
+
+ if tokcnt != numcoins {
+ word := "entries"
+ if tokcnt == 1 {
+ word = "entry"
+ }
+ return Set{}, SyntaxError{
+ expected: fmt.Sprintf("%d mintage entries", numcoins),
+ got: fmt.Sprintf("%d %s", tokcnt, word),
+ file: file,
+ linenr: linenr,
+ }
+ }
+
+ row := Row{Mintmark: mintmark.s}
+ if len(*slice) == 0 {
+ row.Year = data.StartYear
+ } else {
+ row.Year = (*slice)[len(*slice)-1].Year
+ if row.Mintmark == "" || mintmark.star {
+ row.Year++
+ }
+ }
+
+ for i, tok := range tokens {
+ if tok == "?" {
+ row.Cols[i] = Unknown
+ } else {
+ row.Cols[i] = atoiWithDots(tok)
+ }
+ }
+ *slice = append(*slice, row)
+ default:
+ return Set{}, SyntaxError{
+ expected: "‘BEGIN’ directive or mintage row",
+ got: fmt.Sprintf("invalid token ‘%s’", tokens[0]),
+ file: file,
+ linenr: linenr,
+ }
+ }
+ }
+
+ 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 isLabel(s string) bool {
+ n := len(s)
+ return (n > 2 && s[n-1] == ':' && s[n-2] == '*') ||
+ (n > 1 && s[n-1] == ':')
+}
+
+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/lib/mintage/parser_test.go b/lib/mintage/parser_test.go
new file mode 100644
index 0000000..dd78c71
--- /dev/null
+++ b/lib/mintage/parser_test.go
@@ -0,0 +1,280 @@
+package mintage
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+)
+
+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)
+ }
+
+ for i, row := range data.Circ {
+ for j, col := range row.Cols {
+ var n int
+ if i == 1 && j == 1 {
+ n = -1
+ } else {
+ 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.Cols {
+ var n int
+ if i == 1 && j == 1 {
+ n = -1
+ } else {
+ 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.Cols {
+ var n int
+ if i == 1 && j == 1 {
+ n = -1
+ } else {
+ n = 1000*i + j + 1200
+ }
+ if col != n {
+ t.Fatalf("Expected data.Proof[i][j]=%d; got %d", n, col)
+ }
+ }
+ }
+
+ if len(data.Circ) != 2 {
+ t.Fatalf("Expected len(data.Circ)=2; got %d", len(data.Circ))
+ }
+ if len(data.BU) != 2 {
+ t.Fatalf("Expected len(data.BU)=2; got %d", len(data.BU))
+ }
+ if len(data.Proof) != 2 {
+ t.Fatalf("Expected len(data.Proof)=2; got %d", len(data.Proof))
+ }
+}
+
+func TestParserNoProof(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
+ `)), "-")
+
+ if err != nil {
+ t.Fatalf(`Expected err=nil; got "%s"`, err)
+ }
+
+ if len(data.Proof) != 0 {
+ t.Fatalf("Expected len(data.Proof)=0; got %d", len(data.Proof))
+ }
+}
+
+func TestParserMintmarks(t *testing.T) {
+ data, err := Parse(bytes.NewBuffer([]byte(`
+ BEGIN 2020
+ BEGIN CIRC
+ 1.000 1001 1002 1003 1004 1005 1006 1007
+ KNM*: 2000 ? 2002 2003 2004 2005 2006 2007
+ MdP: 3000 ? 3002 3003 3004 3005 3006 3007
+ `)), "-")
+
+ if err != nil {
+ t.Fatalf(`Expected err=nil; got "%s"`, err)
+ }
+
+ for i, row := range data.Circ {
+ for j, col := range row.Cols {
+ var n int
+ if i > 0 && j == 1 {
+ n = -1
+ } else {
+ n = 1000*i + j + 1000
+ }
+ if col != n {
+ t.Fatalf("Expected data.Circ[i][j]=%d; got %d", n, col)
+ }
+ }
+ }
+
+ for i, y := range [...]int{2020, 2021, 2021} {
+ if data.Circ[i].Year != y {
+ t.Fatalf("Expected data.Circ[%d].Year=%d; got %d",
+ i, y, data.Circ[i].Year)
+ }
+ }
+ for i, s := range [...]string{"", "KNM", "MdP"} {
+ if data.Circ[i].Mintmark != s {
+ t.Fatalf(`Expected data.Circ[%d].Mintmark="%s"; got "%s"`,
+ i, s, data.Circ[i].Mintmark)
+ }
+ }
+}
+
+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
+ 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)
+ }
+}