"fmt"
"math"
"strconv"
+ "strings"
)
type EpisodeContainer interface {
// Returns true if this EpisodeContainer is equivalent or a superset of the given EpisodeContainer
ContainsEpisodes(EpisodeContainer) bool
+ // Returns a channel meant for iterating with for/range.
+ // Sends all contained episodes in order.
+ Episodes() chan Episode
}
type Formatter interface {
// Returns a string where the number portion is 0-padded to fit 'width' digits
Format(width int) string
+
+ // Returns a string where the number portion is 0-padded to be the same length
+ // as max.
+ FormatLog(max int) string
}
type EpisodeType int
type Episode struct {
Type EpisodeType
Number int
+ Part int
+ Parts int
+}
+
+// returns how many digits are needed to represent this int
+func scale(i int) int {
+ return 1 + int(math.Floor(math.Log10(float64(i))))
}
// Converts the Episode into AniDB API episode format.
-func (ep *Episode) String() string {
+func (ep Episode) String() string {
return ep.Format(1)
}
if ep == nil {
return 1
}
- return 1 + int(math.Floor(math.Log10(float64(ep.Number))))
+ return scale(ep.Number)
+}
+
+func (ep *Episode) Episodes() chan Episode {
+ ch := make(chan Episode, 1)
+ if ep != nil {
+ ch <- *ep
+ }
+ close(ch)
+ return ch
}
// Returns true if ec is an Episode and is identical to this episode,
func (ep *Episode) ContainsEpisodes(ec EpisodeContainer) bool {
switch e := ec.(type) {
case *Episode:
- return ep != nil && ep.Type == e.Type && ep.Number == ep.Number
+ if ep == nil {
+ return false
+ }
+ basic := ep.Type == e.Type && ep.Number == e.Number
+ if ep.Part < 0 { // a whole episode contains any partial episodes
+ return basic
+ }
+ return basic && ep.Part == e.Part
case *EpisodeRange:
case *EpisodeList:
return EpisodeList{&EpisodeRange{Type: ep.Type, Start: ep, End: ep}}.ContainsEpisodes(ep)
return false
}
-func (ep *Episode) Format(width int) string {
- return fmt.Sprintf("%s%0"+strconv.Itoa(width)+"d", ep.Type, ep.Number)
+func (ep Episode) Format(width int) string {
+ if ep.Part < 0 { // whole episode
+ return fmt.Sprintf("%s%0"+strconv.Itoa(width)+"d", ep.Type, ep.Number)
+ }
+ if ep.Parts != 0 { // part X of Y
+ frac := float64(ep.Number) + float64(ep.Part)/float64(ep.Parts)
+
+ return fmt.Sprintf("%s%0"+strconv.Itoa(width)+".2f", ep.Type, frac)
+ }
+ // part N
+ return fmt.Sprintf("%s%0"+strconv.Itoa(width)+"d.%d", ep.Type, ep.Number, ep.Part)
+}
+
+func (ep *Episode) FormatLog(max int) string {
+ return ep.Format(scale(max))
+}
+
+func (ep *Episode) IncPart() {
+ if ep.Parts > 0 && ep.Part == ep.Parts-1 {
+ ep.IncNumber()
+ } else {
+ ep.Part++
+ }
+}
+
+func (ep *Episode) IncNumber() {
+ ep.Part = -1
+ ep.Parts = 0
+ ep.Number++
+}
+
+func (ep *Episode) DecPart() {
+ if ep.Part > 0 {
+ ep.Part--
+ } else {
+ ep.DecNumber()
+ }
+}
+
+func (ep *Episode) DecNumber() {
+ ep.Part = -1
+ ep.Parts = 0
+ ep.Number--
}
// Parses a string in the usual AniDB API episode format and converts into
// an Episode.
func ParseEpisode(s string) *Episode {
+ p := int64(-1)
+
+ parts := strings.Split(s, ".")
+ switch len(parts) {
+ case 1: // no worries
+ case 2:
+ s = parts[0]
+ p, _ = strconv.ParseInt(parts[1], 10, 32)
+ default: // too many dots
+ return nil
+ }
+
if no, err := strconv.ParseInt(s, 10, 32); err == nil {
- return &Episode{Type: EpisodeTypeRegular, Number: int(no)}
+ return &Episode{Type: EpisodeTypeRegular, Number: int(no), Part: int(p)}
} else if len(s) < 1 {
// s too short
} else if no, err = strconv.ParseInt(s[1:], 10, 30); err == nil {
- return &Episode{Type: parseEpisodeType(s[:1]), Number: int(no)}
+ return &Episode{Type: parseEpisodeType(s[:1]), Number: int(no), Part: int(p)}
}
return nil
}