diff options
Diffstat (limited to 'src/mintage')
| -rw-r--r-- | src/mintage/parser.go | 297 | ||||
| -rw-r--r-- | src/mintage/parser_test.go | 233 | 
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) +	} +} |