]> git.lizzy.rs Git - go-anidb.git/blob - misc/episoderange.go
misc: Add FormatLog method to Episode/EpisodeList/EpisodeRange
[go-anidb.git] / misc / episoderange.go
1 package misc
2
3 import (
4         "fmt"
5         "strings"
6 )
7
8 // A range of episodes with a start and possibly without an end.
9 type EpisodeRange struct {
10         Type  EpisodeType // Must be equal to both the Start and End types; if End is nil, must be equal to the Start type
11         Start *Episode    // The start of the range
12         End   *Episode    // The end of the range; may be nil, which represents an endless range
13 }
14
15 // Converts the EpisodeRange into AniDB API range format.
16 func (er *EpisodeRange) String() string {
17         return er.Format(er.scale())
18 }
19
20 func (er *EpisodeRange) Format(width int) string {
21         if er.Start == er.End || (er.End != nil && *(er.Start) == *(er.End)) {
22                 return er.Start.Format(width)
23         }
24
25         if er.End == nil {
26                 return fmt.Sprintf("%s-", er.Start.Format(width))
27         }
28         return fmt.Sprintf("%s-%s", er.Start.Format(width), er.End.Format(width))
29 }
30
31 func (er *EpisodeRange) FormatLog(max int) string {
32         return er.Format(scale(max))
33 }
34
35 func (er *EpisodeRange) scale() int {
36         if er == nil {
37                 return 1
38         }
39         s, e := er.Start.scale(), er.End.scale()
40         if e > s {
41                 return e
42         }
43         return s
44 }
45
46 // If ec is an *Episode, returns true if the Episode is of the same type as the range
47 // and has a Number >= Start.Number; if End is defined, then the episode's Number must
48 // also be <= End.Number.
49 //
50 // If ec is an *EpisodeRange, returns true if they are both of the same type and
51 // the ec's Start.Number is >= this range's Start.Number;
52 // also returns true if this EpisodeRange is unbounded or if the ec is bounded
53 // and ec's End.Number is <= this range's End.Number.
54 //
55 // If ec is an EpisodeList, returns true if all listed EpisodeRanges are contained
56 // by this EpisodeRange.
57 //
58 // Returns false otherwise.
59 func (er *EpisodeRange) ContainsEpisodes(ec EpisodeContainer) bool {
60         if er == nil {
61                 return false
62         }
63         if er.Start == nil || er.Start.Type != er.Type ||
64                 (er.End != nil && er.End.Type != er.Type) {
65                 panic("Invalid EpisodeRange used")
66         }
67
68         switch e := ec.(type) {
69         case *Episode:
70                 if e.Type == er.Type && e.Number >= er.Start.Number {
71                         if er.End == nil {
72                                 return true
73                         } else if e.Number <= er.End.Number {
74                                 return true
75                         }
76                 }
77         case *EpisodeRange:
78                 if e.Type == er.Type {
79                         if e.Start.Number >= er.Start.Number {
80                                 if er.End == nil {
81                                         return true
82                                 } else if e.End == nil {
83                                         return false // a finite set can't contain an infinite one
84                                 } else if e.End.Number <= er.End.Number {
85                                         return true
86                                 }
87                         }
88                 }
89         case EpisodeList:
90                 for _, ec := range e {
91                         if !er.ContainsEpisodes(ec) {
92                                 return false
93                         }
94                 }
95                 return true
96         default:
97         }
98         return false
99 }
100
101 // Tries to merge a with b, returning a new *EpisodeRange that's
102 // a superset of both a and b.
103 //
104 // Returns nil if a and b don't intersect, or are not adjacent.
105 func (a *EpisodeRange) Merge(b *EpisodeRange) (c *EpisodeRange) {
106         if a.touches(b) {
107                 c = &EpisodeRange{Type: a.Type}
108
109                 if a.Start.Number <= b.Start.Number {
110                         c.Start = a.Start
111                 } else {
112                         c.Start = b.Start
113                 }
114
115                 switch {
116                 case a.End == nil || b.End == nil:
117                         c.End = nil
118                 case a.End.Number >= b.End.Number:
119                         c.End = a.End
120                 default:
121                         c.End = b.End
122                 }
123         }
124         return
125 }
126
127 // Returns true if both ranges are of the same type and
128 // have identical start/end positions
129 func (a *EpisodeRange) Equals(b *EpisodeRange) bool {
130         if a == b { // pointers to the same thing
131                 return true
132         }
133         if a == nil || b == nil {
134                 return false
135         }
136
137         if a.Type == b.Type {
138                 if a.End == b.End || (a.End != nil && b.End != nil && a.End.Number == b.End.Number) {
139                         if a.Start == b.Start || a.Start.Number == b.Start.Number {
140                                 return true
141                         }
142                 }
143         }
144         return false
145 }
146
147 func (a *EpisodeRange) touches(b *EpisodeRange) bool {
148         if a == nil || b == nil || a.Type != b.Type {
149                 return false
150         }
151
152         switch {
153         case a.End == nil:
154                 switch {
155                 case b.End == nil:
156                         // both infinite
157                         return true
158
159                 case b.End.Number >= a.Start.Number-1:
160                         // {b  [ }  a ...
161                         // start-1 so it's still true when they're only adjacent
162                         return true
163                 }
164
165         case b.End == nil:
166                 switch {
167                 case a.End.Number >= b.Start.Number-1:
168                         // [a  { ]  b ...
169                         return true
170                 }
171
172         case a.Start.Number == b.Start.Number || a.End.Number == b.End.Number:
173                 // touching
174                 return true
175
176         case a.End.Number < b.End.Number:
177                 switch {
178                 case a.End.Number >= b.Start.Number-1:
179                         // [a  { ]  b}
180                         return true
181                 }
182
183         case b.End.Number < a.End.Number:
184                 switch {
185                 case b.End.Number >= a.Start.Number-1:
186                         // {b  [ }  a]
187                         return true
188                 }
189         }
190         return false
191 }
192
193 // Parses a string in the AniDB API range format and converts into an EpisodeRange.
194 func ParseEpisodeRange(s string) *EpisodeRange {
195         parts := strings.Split(s, "-")
196         if len(parts) > 2 {
197                 return nil
198         }
199
200         eps := [2]*Episode{}
201         for i := range parts {
202                 eps[i] = ParseEpisode(parts[i])
203         }
204         if eps[0] == nil {
205                 return nil
206         }
207
208         // Not an interval (just "epno") --
209         // convert into interval starting and ending in the same episode
210         if len(parts) == 1 {
211                 eps[1] = eps[0]
212         }
213
214         if len(parts) > 1 && eps[1] != nil && eps[0].Type != eps[1].Type {
215                 return nil
216         }
217         return &EpisodeRange{
218                 Type:  eps[0].Type,
219                 Start: eps[0],
220                 End:   eps[1],
221         }
222 }