]> git.lizzy.rs Git - go-anidb.git/blobdiff - misc/episode.go
anidb: Correct cache key for LID.MyListEntry
[go-anidb.git] / misc / episode.go
index 4a4077d4fd208e7bd85d4b7b935321c51fb9fc8e..920ae93c8481a6cfea1f37be1031e4c0b30d4f2c 100644 (file)
@@ -2,6 +2,7 @@ package misc
 
 import (
        "fmt"
+       "math"
        "strconv"
        "strings"
 )
@@ -9,10 +10,18 @@ import (
 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
@@ -67,178 +76,125 @@ func (et EpisodeType) String() string {
 type Episode struct {
        Type   EpisodeType
        Number int
+       Part   int
+       Parts  int
 }
 
-// Converts the Episode into AniDB API episode format.
-func (ep *Episode) String() string {
-       return fmt.Sprintf("%s%d", ep.Type, ep.Number)
-}
-
-// 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
-       default:
-       }
-       return false
+// returns how many digits are needed to represent this int
+func scale(i int) int {
+       return 1 + int(math.Floor(math.Log10(float64(i))))
 }
 
-func (ep *Episode) Format(width int) string {
-       return fmt.Sprintf("%s%0"+strconv.Itoa(width)+"d", ep.Type, ep.Number)
+// Converts the Episode into AniDB API episode format.
+func (ep Episode) String() string {
+       return ep.Format(1)
 }
 
-// Parses a string in the usual AniDB API episode format and converts into
-// an Episode.
-//
-//     ParseEpisode("1")  <=> &Episode{Type: EpisodeTypeRegular, Number: 1}
-//     ParseEpisode("S2") <=> &Episode{Type: EpisodeTypeSpecial, Number: 2}
-//     ParseEpisode("03") <=> &Episode{Type: EpisodeTypeRegular, Number: 3}
-//     ParseEpisode("")   <=> nil // invalid number
-func ParseEpisode(s string) *Episode {
-       if no, err := strconv.ParseInt(s, 10, 32); err == nil {
-               return &Episode{Type: EpisodeTypeRegular, Number: int(no)}
-       } else if no, err = strconv.ParseInt(s[1:], 10, 30); err == nil {
-               return &Episode{Type: parseEpisodeType(s[:1]), Number: int(no)}
+// returns how many digits are needed to represent this episode
+func (ep *Episode) scale() int {
+       if ep == nil {
+               return 1
        }
-       return nil
+       return scale(ep.Number)
 }
 
-// A range of episodes with a start and possibly without an end.
-type EpisodeRange struct {
-       Type  EpisodeType // Must be equal to both the Start and End types, unless End is nil
-       Start *Episode    // The start of the range
-       End   *Episode    // The end of the range; may be nil, which represents an endless range
-}
-
-// Converts the EpisodeRange into AniDB API range format.
-func (ei *EpisodeRange) String() string {
-       if ei.End == nil || ei.Start == ei.End || *(ei.Start) == *(ei.End) {
-               return ei.Start.String()
-       }
-       return fmt.Sprintf("%s-%s", ei.Start, ei.End)
-}
-
-// If ec is an Episode, returns true if the Episode is of the same type as the range
-// and has a Number >= Start.Number; if End is defined, then the episode's Number must
-// also be <= End.Number.
-//
-// If ec is an EpisodeRange, returns true if they are both of the same type and
-// the ec's Start.Number is >= this range's Start.Number;
-// also returns true if this EpisodeRange is unbounded or if the ec is bounded
-// and ec's End.Number is <= this range's End.Number.
-//
-// Returns false otherwise.
-func (er *EpisodeRange) ContainsEpisodes(ec EpisodeContainer) bool {
-       if er == nil {
-               return false
-       }
-       if er.Start == nil || er.Start.Type != e.Type ||
-               (er.End != nil && er.End.Type != e.Type) {
-               panic("Invalid EpisodeRange used")
+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,
+// or if ec is a single episode EpisodeRange / EpisodeList that
+// contain only this episode.
+func (ep *Episode) ContainsEpisodes(ec EpisodeContainer) bool {
        switch e := ec.(type) {
        case *Episode:
-               if e.Type == er.Type && e.Number >= er.Start.Number {
-                       if er.End == nil {
-                               return true
-                       } else if e.Number <= er.End.Number {
-                               return true
-                       }
+               if ep == nil {
+                       return false
                }
-       case *EpisodeRange:
-               if e.Type == er.Type {
-                       if e.Start.Number >= er.Start.Number {
-                               if er.End == nil {
-                                       return true
-                               } else if e.End == nil {
-                                       return false // a finite set can't contain an infinite one
-                               } else if e.End.Number <= er.End.Number {
-                                       return true
-                               }
-                       }
+               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)
        default:
        }
        return false
 }
 
-// Parses a string in the AniDB API range format and converts into an EpisodeRange.
-//
-//     ParseEpisodeRange("1")     <=> ep := ParseEpisode("1");
-//             &EpisodeRange{Type: EpisodeTypeRegular, Start: ep, End: ep}
-//     ParseEpisodeRange("S1-")   <=>
-//             &EpisodeRange{Type: EpisodeTypeSpecial, Start: ParseEpisode("S1")}
-//     ParseEpisodeRange("T1-T3") <=>
-//             &EpisodeRange{Type: EpisodeTypeTrailer, Start: ParseEpisode("T1"), End: ParseEpisode("T3")}
-//     ParseEpisodeRange("5-S3")  <=> nil // different episode types in range
-//     ParseEpisodeRange("")      <=> nil // invalid start of range
-func ParseEpisodeRange(s string) *EpisodeRange {
-       parts := strings.Split(s, "-")
-       if len(parts) > 2 {
-               return nil
+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)
 
-       eps := [2]*Episode{}
-       for i := range parts {
-               eps[i] = ParseEpisode(parts[i])
-       }
-       if eps[0] == nil {
-               return nil
+               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)
+}
 
-       // Not an interval (just "epno") --
-       // convert into interval starting and ending in the same episode
-       if len(parts) == 1 {
-               eps[1] = eps[0]
-       }
+func (ep *Episode) FormatLog(max int) string {
+       return ep.Format(scale(max))
+}
 
-       if len(parts) > 1 && eps[1] != nil && eps[0].Type != eps[1].Type {
-               return nil
-       }
-       return &EpisodeRange{
-               Type:  eps[0].Type,
-               Start: eps[0],
-               End:   eps[1],
+func (ep *Episode) IncPart() {
+       if ep.Parts > 0 && ep.Part == ep.Parts-1 {
+               ep.IncNumber()
+       } else {
+               ep.Part++
        }
 }
 
-type EpisodeList []*EpisodeRange
-
-// Converts the EpisodeList into the AniDB API list format.
-func (el EpisodeList) String() string {
-       parts := make([]string, len(el))
-       for i, er := range el {
-               parts[i] = er.String()
-       }
-       return strings.Join(parts, ",")
+func (ep *Episode) IncNumber() {
+       ep.Part = -1
+       ep.Parts = 0
+       ep.Number++
 }
 
-// Returns true if any of the contained EpisodeRanges contain the
-// given EpisodeContainer.
-func (el EpisodeList) ContainsEpisodes(ec EpisodeContainer) bool {
-       for _, i := range el {
-               if i != nil && i.ContainsEpisodes(ec) {
-                       return true
-               }
+func (ep *Episode) DecPart() {
+       if ep.Part > 0 {
+               ep.Part--
+       } else {
+               ep.DecNumber()
        }
-       return false
 }
 
-// Parses a string in the AniDB API list format and converts into
-// an EpisodeList.
-//
-//     ParseEpisodeList("01")       <=> EpisodeList{ParseEpisodeRange("01")}
-//     ParseEpisodeList("S2-S3")    <=> EpisodeList{ParseEpisodeRange("S2-S3")}
-//     ParseEpisodeList("T1,C1-C3") <=> EpisodeList{ParseEpisodeRange("T1"), ParseEpisodeRange("C1-C3")}
-func ParseEpisodeList(s string) (el EpisodeList) {
-       parts := strings.Split(s, ",")
+func (ep *Episode) DecNumber() {
+       ep.Part = -1
+       ep.Parts = 0
+       ep.Number--
+}
 
-       el = make(EpisodeList, len(parts))
-       for i := range parts {
-               el[i] = ParseEpisodeRange(parts[i])
+// 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
        }
 
-       return
+       if no, err := strconv.ParseInt(s, 10, 32); err == nil {
+               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), Part: int(p)}
+       }
+       return nil
 }