Serge Bazanski | 8ef457f | 2021-07-11 14:42:38 +0000 | [diff] [blame] | 1 | package calendar |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sync" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/golang/glog" |
| 9 | ) |
| 10 | |
| 11 | // UpcomingEvent is a calendar event that will happen in the near future, or is |
| 12 | // currently happening (relative to same arbitrary timestamp of 'now', |
| 13 | // depending on the way the UpcomingEvent is crated). |
| 14 | // |
| 15 | // It is a best-effort parse of an ICS/iCal event into some event that can be |
| 16 | // interpreted as a 'community event', to be displayed publicly on a site. |
| 17 | type UpcomingEvent struct { |
| 18 | // UID is the unique ICS/iCal ID of this event. |
| 19 | UID string |
| 20 | // Summary is the 'title' of the event, usually a short one-liner. |
| 21 | Summary string |
Serge Bazanski | 717aad4 | 2021-07-11 16:03:43 +0000 | [diff] [blame] | 22 | // Full description of event. Might contain multiple lines of test. |
| 23 | Description string |
Serge Bazanski | 8ef457f | 2021-07-11 14:42:38 +0000 | [diff] [blame] | 24 | // Start and End of the events, potentially whole-day dates. See EventTime |
| 25 | // for more information. |
| 26 | // If Start is WholeDay then so is End, and vice-versa. |
| 27 | Start *EventTime |
| 28 | // End of the event, exclusive of the time range (ie. if a timestamp it |
| 29 | // defines the timestamp at which the next event can start; if it's whole |
| 30 | // day it defines the first day on which the event does not take place). |
| 31 | End *EventTime |
| 32 | // Tentative is whether this event is marked as 'Tentative' in the source |
| 33 | // calendar. |
| 34 | Tentative bool |
| 35 | } |
| 36 | |
| 37 | // WholeDay returns true if this is a whole-day (or multi-day) event. |
| 38 | func (u *UpcomingEvent) WholeDay() bool { |
| 39 | return u.Start.WholeDay |
| 40 | } |
| 41 | |
| 42 | var ( |
| 43 | // onceComplainWarsawGone gates throwing a very verbose message about being |
| 44 | // unable to localize UpcomingEvents into Warsaw local time by WarsawDate. |
| 45 | onceComplainWarsawGone sync.Once |
| 46 | ) |
| 47 | |
| 48 | // WarsawDate prints a human-readable timestamp that makes sense within the |
| 49 | // context of this event taking place in Warsaw, or at least in the same |
| 50 | // timezone as Warsaw. |
| 51 | // It will return a time in one of the following formats: |
| 52 | // |
| 53 | // YEAR/MONTH/DAY |
| 54 | // (For one-day events) |
| 55 | // |
| 56 | // YEAR/MONTH/DAY - DAY |
| 57 | // (For multi-day events within the same month) |
| 58 | // |
| 59 | // YEAR/MONTH/DAY - YEAR/MONTH/DAY |
| 60 | // (For multi-day events spanning more than one month) |
| 61 | // |
| 62 | // YEAR/MONTH/DAY HH:MM - HH:MM |
| 63 | // (For timestamped events within the same day) |
| 64 | // |
| 65 | // YEAR/MONTH/DAY HH:MM - YEAR/MONTH/DAY HH:MM |
| 66 | // (For timestamped events spanning more than one day) |
| 67 | // |
| 68 | func (u *UpcomingEvent) WarsawDate() string { |
| 69 | YM := "2006/01" |
| 70 | D := "02" |
| 71 | YMD := "2006/01/02" |
| 72 | HM := "15:04" |
| 73 | YMDHM := "2006/01/02 15:04" |
| 74 | |
| 75 | if u.WholeDay() { |
| 76 | start := u.Start.Time |
| 77 | // ICS whole-day dates are [start, end), ie. 'end' is exclusive. |
| 78 | end := u.End.Time.AddDate(0, 0, -1) |
| 79 | if start == end { |
| 80 | // Event is one-day. |
| 81 | return start.Format(YMD) |
| 82 | } |
| 83 | if start.Year() == end.Year() && start.Month() == end.Month() { |
| 84 | // Event starts and ends on the same month, print shortened form. |
| 85 | return fmt.Sprintf("%s/%s - %s", start.Format(YM), start.Format(D), end.Format(D)) |
| 86 | } |
| 87 | // Event spans multiple months, print full form. |
| 88 | return fmt.Sprintf("%s - %s", start.Format(YMD), end.Format(YMD)) |
| 89 | } |
| 90 | |
| 91 | warsaw, err := time.LoadLocation("Europe/Warsaw") |
| 92 | if err != nil { |
| 93 | onceComplainWarsawGone.Do(func() { |
| 94 | glog.Errorf("Could not load Europe/Warsaw timezone, did the city cease to exist? LoadLoaction: %v", err) |
| 95 | }) |
| 96 | // Even in the face of a cataclysm, degrade gracefully and assume the |
| 97 | // users are local to this service's timezone. |
| 98 | warsaw = time.Local |
| 99 | } |
| 100 | |
| 101 | start := u.Start.Time.In(warsaw) |
| 102 | end := u.End.Time.In(warsaw) |
| 103 | if start.Year() == end.Year() && start.Month() == end.Month() && start.Day() == end.Day() { |
| 104 | // Event starts and ends on same day, print shortened form. |
| 105 | return fmt.Sprintf("%s %s - %s", start.Format(YMD), start.Format(HM), end.Format(HM)) |
| 106 | } |
| 107 | // Event spans multiple days, print full form. |
| 108 | return fmt.Sprintf("%s - %s", start.Format(YMDHM), end.Format(YMDHM)) |
| 109 | } |
| 110 | |
| 111 | func (u *UpcomingEvent) String() string { |
| 112 | return fmt.Sprintf("%s (%s)", u.Summary, u.WarsawDate()) |
| 113 | } |
| 114 | |
| 115 | func (e *UpcomingEvent) Elapsed(t time.Time) bool { |
| 116 | // Event hasn't started yet? |
| 117 | if e.Start.Time.After(t) { |
| 118 | return false |
| 119 | } |
| 120 | // Event has started, but hasn't ended? |
| 121 | if e.End.Time.After(t) { |
| 122 | return false |
| 123 | } |
| 124 | return true |
| 125 | } |