Repository navigation
Expand file tree
/
Copy pathSchedule.hs
More file actions
234 lines (205 loc) · 6.9 KB
/
Copy pathSchedule.hs
File metadata and controls
234 lines (205 loc) · 6.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Schedule
( nextEvent
, eventSummary
, dayOfWeek
, Schedule(..)
) where
import Control.Monad
import Data.Aeson
import Data.Aeson.Types
import Data.Either.Extra
import Data.Function
import Data.List
import qualified Data.Map as M
import Data.Maybe
import Data.Maybe.Extra
import Data.Monoid
import qualified Data.Text as T
import Data.Time
import Data.Time.Calendar.WeekDate
import Data.Time.Clock.POSIX
import Data.Time.Extra
import Data.Time.LocalTime (TimeZone)
import Safe
import Text.InterpolatedString.QM
data DayOfWeek
= Monday
| Tuesday
| Wednesday
| Thursday
| Friday
| Saturday
| Sunday
deriving (Eq, Ord, Enum, Show, Bounded)
dayOfWeek :: Day -> DayOfWeek
dayOfWeek day =
toEnumNote
[qms|{s} is not a correct week number according to
ISO 8601 Week Date format.|]
(s - 1)
where
(_, _, s) = toWeekDate day
newtype ScheduleTimeZone =
ScheduleTimeZone TimeZone
deriving (Show)
data Project = Project
{ projectName :: T.Text
, projectDescription :: T.Text
, projectUrl :: T.Text
, projectDays :: [DayOfWeek]
, projectTime :: TimeOfDay
, projectChannel :: T.Text
, projectStarts :: Maybe Day
, projectEnds :: Maybe Day
} deriving (Show)
data Event = Event
{ eventDate :: Day
, eventTime :: TimeOfDay
, eventTitle :: T.Text
, eventDescription :: T.Text
, eventUrl :: T.Text
, eventChannel :: T.Text
} deriving (Show)
eventId :: ScheduleTimeZone -> Event -> EventId
eventId timeZone event =
EventId $ floor $ utcTimeToPOSIXSeconds $ eventUTCTime timeZone event
eventUTCTime :: ScheduleTimeZone -> Event -> UTCTime
eventUTCTime (ScheduleTimeZone timeZone) Event { eventDate = day
, eventTime = timeOfDay
} =
localTimeToUTC timeZone localTime
where
localTime = LocalTime day timeOfDay
eventSummary :: ScheduleTimeZone -> UTCTime -> Event -> T.Text
eventSummary timezone now event = do
let t = eventUTCTime timezone event
-- TODO(#755): Diff time for past events in eventSummary should be `finished - (started + DURATION)`
if t >= now
then let d = diffUTCTime t now
in [qms|{eventTitle event}
starts in {humanReadableDiffTime d}|]
else let d = diffUTCTime now t
in [qms|{eventTitle event}
finished {humanReadableDiffTime d} ago|]
newtype EventId =
EventId Int
deriving (Eq, Ord, Show)
data EventPatch = EventPatch
{ eventPatchTitle :: Maybe T.Text
, eventPatchDescription :: Maybe T.Text
, eventPatchUrl :: Maybe T.Text
, eventPatchChannel :: Maybe T.Text
} deriving (Show)
data Schedule = Schedule
{ scheduleProject :: [Project]
, scheduleExtraEvents :: [Event]
, scheduleCancelledEvents :: [EventId]
, scheduleTimezone :: ScheduleTimeZone
, schedulePatches :: M.Map EventId EventPatch
} deriving (Show)
instance FromJSON Schedule where
parseJSON (Object v) =
Schedule <$> v .: "projects" <*> v .: "extraEvents" <*>
v .: "cancelledEvents" <*>
v .: "timezone" <*>
v .: "eventPatches"
parseJSON invalid = typeMismatch "Schedule" invalid
instance FromJSON Project where
parseJSON (Object v) =
Project <$> v .: "name" <*> v .: "description" <*> v .: "url" <*>
v .: "days" <*>
v .: "time" <*>
v .: "channel" <*>
v .:? "starts" <*>
v .:? "ends"
parseJSON invalid = typeMismatch "Project" invalid
instance FromJSON Event where
parseJSON (Object v) =
Event <$> v .: "date" <*> v .: "time" <*> v .: "title" <*>
v .: "description" <*>
v .: "url" <*>
v .: "channel"
parseJSON invalid = typeMismatch "Event" invalid
instance FromJSON EventId where
parseJSON = fmap EventId . parseJSON
instance FromJSONKey EventId where
fromJSONKey = EventId <$> fromJSONKey
parseTimeZone :: T.Text -> Parser TimeZone
parseTimeZone "Asia/Novosibirsk" = return $ minutesToTimeZone 420
parseTimeZone s = fail ("Unknown timezone: " ++ T.unpack s)
instance FromJSON ScheduleTimeZone where
parseJSON (String s) = ScheduleTimeZone <$> parseTimeZone s
parseJSON invalid = typeMismatch "ScheduleTimeZone" invalid
instance FromJSON EventPatch where
parseJSON (Object v) =
EventPatch <$> v .:? "title" <*> v .:? "description" <*> v .:? "url" <*>
v .:? "channel"
parseJSON invalid = typeMismatch "EventPatch" invalid
instance FromJSON DayOfWeek where
parseJSON =
maybeFail "Unknown day of week" . toEnumMay . (\x -> x - 1) <=< parseJSON
cancelEvents :: ScheduleTimeZone -> [EventId] -> [Event] -> [Event]
cancelEvents timeZone cancelledIds =
filter (\e -> eventId timeZone e `notElem` cancelledIds)
makeEvent :: Day -> Project -> Event
makeEvent day project =
Event
{ eventDate = day
, eventTime = projectTime project
, eventTitle = projectName project
, eventDescription = projectDescription project
, eventUrl = projectUrl project
, eventChannel = projectChannel project
}
between :: Day -> Project -> Bool
between day project =
getAll $
fromMaybe (All True) $
mappend
(All . (<= day) <$> projectStarts project)
(All . (day <=) <$> projectEnds project)
projectsOfDay :: Day -> [Project] -> [Event]
projectsOfDay day projects =
map (makeEvent day) $
filter (\p -> weekDay `elem` projectDays p && between day p) projects
where
weekDay = dayOfWeek day
recurringEventsFrom :: Day -> [Project] -> [Event]
recurringEventsFrom day projects =
projectsOfDay day projects ++ recurringEventsFrom (succ day) projects
eventsFrom :: Day -> Schedule -> [Event]
eventsFrom day schedule@Schedule { scheduleTimezone = timezone
, scheduleCancelledEvents = cancelledIds
, schedulePatches = patches
} =
sortBy
(compare `on` eventDate)
(map patchEvent (extraEvents <> recurringEvents))
where
recurringEvents =
take 100 $
cancelEvents timezone cancelledIds $
recurringEventsFrom day $ scheduleProject schedule
extraEvents =
cancelEvents timezone cancelledIds $
filter ((>= day) . eventDate) $ scheduleExtraEvents schedule
applyPatch :: Event -> EventPatch -> Event
applyPatch event patch =
event
{ eventTitle = fromMaybe (eventTitle event) (eventPatchTitle patch)
, eventDescription =
fromMaybe (eventDescription event) (eventPatchDescription patch)
, eventUrl = fromMaybe (eventUrl event) (eventPatchUrl patch)
, eventChannel =
fromMaybe (eventChannel event) (eventPatchChannel patch)
}
patchEvent :: Event -> Event
patchEvent event =
maybe event (applyPatch event) $ M.lookup (eventId timezone event) patches
nextEvent :: Schedule -> UTCTime -> Either String Event
nextEvent schedule timePoint =
maybeToEither "No events found" $
find ((> timePoint) . eventUTCTime (scheduleTimezone schedule)) $
eventsFrom (utctDay timePoint) schedule