aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-09-21 21:24:47 +0200
committerMartin Polden <mpolden@mpolden.no>2021-09-22 19:16:01 +0200
commite53bfa3aeb134ae3a00c28446d2b18eda5ffdbf4 (patch)
tree55973a04d9fe6de01e94829fc2b644316bb798fe
parent5498f85304eda7e6b70789a7ca2264fb8daf2160 (diff)
parser: Support episode numbers using roman numerals
-rw-r--r--parser/parser.go78
-rw-r--r--parser/parser_test.go48
2 files changed, 123 insertions, 3 deletions
diff --git a/parser/parser.go b/parser/parser.go
index 1404542..a2087bf 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -17,7 +17,7 @@ var (
regexp.MustCompile(`^(?P<name>.+?)\.S(?P<season>\d{2})(?:E(?P<episode>\d{2}))?`), // S01, S01E04
regexp.MustCompile(`^(?P<name>.+?)\.E(?P<episode>\d{2})`), // E04
regexp.MustCompile(`^(?P<name>.+?)\.(?P<season>\d{1,2})x(?P<episode>\d{2})`), // 1x04, 01x04
- regexp.MustCompile(`^(?P<name>.+?)\.Part\.?(?P<episode>\d{1,2})`), // Part4, Part11, Part.4, Part.11
+ regexp.MustCompile(`^(?P<name>.+?)\.P(?:ar)?t\.?(?P<episode>([^.]+))`), // P(ar)t(.)11, Pt(.)XI
}
splitPattern = regexp.MustCompile(`[-_.]`)
)
@@ -114,12 +114,15 @@ func Show(s string) (Media, error) {
case "season":
season, err = strconv.Atoi(group)
if err != nil {
- return Media{}, fmt.Errorf("invalid input: %q: %s", s, err)
+ return Media{}, fmt.Errorf("invalid input: %q: %w", s, err)
}
case "episode":
episode, err = strconv.Atoi(group)
if err != nil {
- return Media{}, fmt.Errorf("invalid input: %q: %s", s, err)
+ episode, err = rtoi(group)
+ if err != nil {
+ return Media{}, fmt.Errorf("invalid input: %q: %w", s, err)
+ }
}
}
}
@@ -165,3 +168,72 @@ func codec(s string) string {
return false
})
}
+
+var numerals = []numeral{
+ // units
+ {"IX", 9},
+ {"VIII", 8},
+ {"VII", 7},
+ {"VI", 6},
+ {"IV", 4},
+ {"III", 3},
+ {"II", 2},
+ {"V", 5},
+ {"I", 1},
+ // tens
+ {"XC", 90},
+ {"LXXX", 80},
+ {"LXX", 70},
+ {"LX", 60},
+ {"XL", 40},
+ {"XXX", 30},
+ {"XX", 20},
+ {"L", 50},
+ {"X", 10},
+ // hundreds
+ {"CM", 900},
+ {"DCCC", 800},
+ {"DCC", 700},
+ {"DC", 600},
+ {"CD", 400},
+ {"CCC", 300},
+ {"CC", 200},
+ {"D", 500},
+ {"C", 100},
+ // thousands
+ {"MMM", 3000},
+ {"MM", 2000},
+ {"M", 1000},
+}
+
+type numeral struct {
+ s string
+ n int
+}
+
+func (n numeral) parse(s string, lastIndex int) (string, int, int) {
+ i := strings.LastIndex(s, n.s)
+ if i > -1 && i < lastIndex {
+ rest := s[:i] + s[i+len(n.s):]
+ return rest, i, n.n
+ }
+ return s, lastIndex, 0
+}
+
+func rtoi(s string) (int, error) {
+ // needlessly complete parsing of roman numerals
+ sum := 0
+ lastIndex := len(s)
+ for _, num := range numerals {
+ var n int
+ s, lastIndex, n = num.parse(s, lastIndex)
+ sum += n
+ if s == "" {
+ break
+ }
+ }
+ if s != "" {
+ return 0, fmt.Errorf("invalid roman numeral: %q", s)
+ }
+ return sum, nil
+}
diff --git a/parser/parser_test.go b/parser/parser_test.go
index caa8a02..cb6c3d7 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -172,6 +172,15 @@ func TestShow(t *testing.T) {
Episode: 16,
Codec: "xvid",
}},
+ {"Generation.Kill.Pt.VII.Bomb.in.the.Garden.720p.Bluray.X264-DIMENSION",
+ Media{
+ Release: "Generation.Kill.Pt.VII.Bomb.in.the.Garden.720p.Bluray.X264-DIMENSION",
+ Name: "Generation.Kill",
+ Season: 1,
+ Episode: 7,
+ Resolution: "720p",
+ Codec: "x264",
+ }},
}
for _, tt := range tests {
got, err := Show(tt.in)
@@ -200,3 +209,42 @@ func TestReplaceName(t *testing.T) {
t.Errorf("Expected %q, got %q", want, m.Name)
}
}
+
+func TestRtoi(t *testing.T) {
+ assertRtoiErr(t, "foo")
+ assertRtoiErr(t, "VIM")
+ assertRtoiErr(t, "DMVI")
+
+ assertRtoi(t, "XXXIX", 39)
+ assertRtoi(t, "CCXLVI", 246)
+ assertRtoi(t, "DCCLXXXIX", 789)
+ assertRtoi(t, "MMCDXXI", 2421)
+
+ assertRtoi(t, "CLX", 160)
+ assertRtoi(t, "CCVII", 207)
+ assertRtoi(t, "MIX", 1009)
+ assertRtoi(t, "MLXVI", 1066)
+
+ assertRtoi(t, "MDCCLXXVI", 1776)
+ assertRtoi(t, "MCMXVIII", 1918)
+ assertRtoi(t, "MCMLIV", 1954)
+ assertRtoi(t, "MMXIV", 2014)
+
+ assertRtoi(t, "MMMCMXCIX", 3999)
+}
+
+func assertRtoiErr(t *testing.T, rnum string) {
+ if n, err := rtoi(rnum); err == nil {
+ t.Fatalf("rtoi(%q) = (%d, %v), want error", rnum, n, err)
+ }
+}
+
+func assertRtoi(t *testing.T, rnum string, expected int) {
+ n, err := rtoi(rnum)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != expected {
+ t.Errorf("rtoi(%q) = %d, want %d", rnum, n, expected)
+ }
+}