]> git.lizzy.rs Git - go-anidb.git/blob - misc/episode.go
c5d41a88e3e566beaebbd9a8285947f08b64c596
[go-anidb.git] / misc / episode.go
1 package misc
2
3 import (
4         "fmt"
5         "math"
6         "strconv"
7         "strings"
8 )
9
10 type EpisodeContainer interface {
11         // Returns true if this EpisodeContainer is equivalent or a superset of the given EpisodeContainer
12         ContainsEpisodes(EpisodeContainer) bool
13 }
14
15 type Formatter interface {
16         // Returns a string where the number portion is 0-padded to fit 'width' digits
17         Format(width int) string
18
19         // Returns a string where the number portion is 0-padded to be the same length
20         // as max.
21         FormatLog(max int) string
22 }
23
24 type EpisodeType int
25
26 const (
27         EpisodeTypeRegular = EpisodeType(1 + iota)
28         EpisodeTypeSpecial // "S" episode
29         EpisodeTypeCredits // "C" episode
30         EpisodeTypeTrailer // "T" episode
31         EpisodeTypeParody  // "P" episode
32         EpisodeTypeOther   // "O" episode
33 )
34
35 func parseEpisodeType(typ string) EpisodeType {
36         switch typ {
37         case "":
38                 return EpisodeTypeRegular
39         case "S":
40                 return EpisodeTypeSpecial
41         case "C":
42                 return EpisodeTypeCredits
43         case "T":
44                 return EpisodeTypeTrailer
45         case "P":
46                 return EpisodeTypeParody
47         case "O":
48                 return EpisodeTypeOther
49         }
50         return 0
51 }
52
53 func (et EpisodeType) String() string {
54         switch et {
55         case EpisodeTypeRegular:
56                 return ""
57         case EpisodeTypeSpecial:
58                 return "S"
59         case EpisodeTypeCredits:
60                 return "C"
61         case EpisodeTypeTrailer:
62                 return "T"
63         case EpisodeTypeParody:
64                 return "P"
65         case EpisodeTypeOther:
66                 return "O"
67         default:
68                 return "!"
69         }
70 }
71
72 // An episode (duh).
73 type Episode struct {
74         Type   EpisodeType
75         Number int
76         Part   int
77         Parts  int
78 }
79
80 // returns how many digits are needed to represent this int
81 func scale(i int) int {
82         return 1 + int(math.Floor(math.Log10(float64(i))))
83 }
84
85 // Converts the Episode into AniDB API episode format.
86 func (ep *Episode) String() string {
87         return ep.Format(1)
88 }
89
90 // returns how many digits are needed to represent this episode
91 func (ep *Episode) scale() int {
92         if ep == nil {
93                 return 1
94         }
95         return scale(ep.Number)
96 }
97
98 // Returns true if ec is an Episode and is identical to this episode,
99 // or if ec is a single episode EpisodeRange / EpisodeList that
100 // contain only this episode.
101 func (ep *Episode) ContainsEpisodes(ec EpisodeContainer) bool {
102         switch e := ec.(type) {
103         case *Episode:
104                 if ep == nil {
105                         return false
106                 }
107                 basic := ep.Type == e.Type && ep.Number == e.Number
108                 if ep.Part < 0 { // a whole episode contains any partial episodes
109                         return basic
110                 }
111                 return basic && ep.Part == e.Part
112         case *EpisodeRange:
113         case *EpisodeList:
114                 return EpisodeList{&EpisodeRange{Type: ep.Type, Start: ep, End: ep}}.ContainsEpisodes(ep)
115         default:
116         }
117         return false
118 }
119
120 func (ep *Episode) Format(width int) string {
121         if ep.Part < 0 { // whole episode
122                 return fmt.Sprintf("%s%0"+strconv.Itoa(width)+"d", ep.Type, ep.Number)
123         }
124         if ep.Parts != 0 { // part X of Y
125                 frac := float64(ep.Number) + float64(ep.Part)/float64(ep.Parts)
126
127                 return fmt.Sprintf("%s%0"+strconv.Itoa(width)+".2f", ep.Type, frac)
128         }
129         // part N
130         return fmt.Sprintf("%s%0"+strconv.Itoa(width)+"d.%d", ep.Type, ep.Number, ep.Part)
131 }
132
133 func (ep *Episode) FormatLog(max int) string {
134         return ep.Format(scale(max))
135 }
136
137 func (ep *Episode) IncPart() {
138         if ep.Parts > 0 && ep.Part == ep.Parts-1 {
139                 ep.IncNumber()
140         } else {
141                 ep.Part++
142         }
143 }
144
145 func (ep *Episode) IncNumber() {
146         ep.Part = -1
147         ep.Parts = 0
148         ep.Number++
149 }
150
151 func (ep *Episode) DecPart() {
152         if ep.Part > 0 {
153                 ep.Part--
154         } else {
155                 ep.DecNumber()
156         }
157 }
158
159 func (ep *Episode) DecNumber() {
160         ep.Part = -1
161         ep.Parts = 0
162         ep.Number--
163 }
164
165 // Parses a string in the usual AniDB API episode format and converts into
166 // an Episode.
167 func ParseEpisode(s string) *Episode {
168         p := int64(-1)
169
170         parts := strings.Split(s, ".")
171         switch len(parts) {
172         case 1: // no worries
173         case 2:
174                 s = parts[0]
175                 p, _ = strconv.ParseInt(parts[1], 10, 32)
176         default: // too many dots
177                 return nil
178         }
179
180         if no, err := strconv.ParseInt(s, 10, 32); err == nil {
181                 return &Episode{Type: EpisodeTypeRegular, Number: int(no), Part: int(p)}
182         } else if len(s) < 1 {
183                 // s too short
184         } else if no, err = strconv.ParseInt(s[1:], 10, 30); err == nil {
185                 return &Episode{Type: parseEpisodeType(s[:1]), Number: int(no), Part: int(p)}
186         }
187         return nil
188 }