-
Notifications
You must be signed in to change notification settings - Fork 0
/
ical2text.hs
150 lines (122 loc) · 5.6 KB
/
ical2text.hs
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
{-# LANGUAGE QuasiQuotes #-}
-- Convert iCalendar format to plain text
-- usage: ical2text < cal.ics
-- :set makeprg=ghc\ %
-- :make
-- dependencies: iCalendar, docopt, regex-compat
import Prelude hiding (getContents)
import Text.Regex
import Text.ICalendar
import Text.Printf (printf)
import Data.ByteString.Lazy (getContents)
import Data.Text.Lazy (pack, unpack)
import Data.Default
import Data.Maybe (maybe, fromJust)
import Data.List (intercalate)
import Data.Time.Format (FormatTime, formatTime, defaultTimeLocale, iso8601DateFormat)
import Data.Time.LocalTime (TimeZone (..), getCurrentTimeZone, utcToLocalTime, localTimeToUTC, hoursToTimeZone)
import Data.Time.Clock (UTCTime (..), secondsToDiffTime, diffUTCTime, addUTCTime)
import Data.String.Utils
import System.Posix.Temp
import System.IO (hPutStrLn, stderr)
import System.Environment (getArgs)
import System.Console.Docopt
import Control.Monad (when)
patterns :: Docopt
patterns = [docopt|ical2text - convert iCalendar to plain text
Usage:
ical2text [options]
Options:
-f, --field-separator=STRING
Field separator used to separate title, description, and location [default: @@].
-l, --line-separator=STRING
Line separator used to separate lines in description and location [default: ,,].
-h, --help
Print this help message.
|]
main :: IO ()
main = do
args <- parseArgsOrExit patterns =<< getArgs
when (isPresent args $ longOption "help") $
exitWithUsage patterns
input <- getContents
(fileUsedInErrorMessages, _) <- mkstemp "/tmp/ical2text-"
let result = parseICalendar def fileUsedInErrorMessages input
case result of
Left msg -> printError msg
Right (calendars, warnings) -> do
mapM_ printError warnings
proceedWithCalendars args calendars
getArgOrExit = getArgOrExitWith patterns
proceedWithCalendars :: Arguments -> [VCalendar] -> IO ()
proceedWithCalendars args = mapM_ (printEvents args)
printEvents :: Arguments -> VCalendar -> IO ()
printEvents args c = do
tz <- getCurrentTimeZone
mapM_ (printEvent args tz) $ vcEvents c
printEvent :: Arguments -> TimeZone -> VEvent -> IO ()
printEvent args tz e = do
fieldSep <- getArgOrExit args $ longOption "field-separator"
lineSep <- getArgOrExit args $ longOption "line-separator"
let fields = map (($e) . ($tz)) [startDate, endDate, duration, summary lineSep, description lineSep, location lineSep]
let concatExtraFields = intercalate fieldSep
putStrLn $ unwords (take nSimpleFields fields) ++ " " ++ concatExtraFields (drop nSimpleFields fields)
where
nSimpleFields = 3
startDate :: TimeZone -> VEvent -> String
startDate tz e = maybe "" (formatUTCTime tz . dtStartToUTC tz) $ veDTStart e
endDate :: TimeZone -> VEvent -> String
endDate tz e = case veDTEndDuration e of
Nothing -> ""
Just (Left dt) -> formatUTCTime tz $ dtEndToUTC tz dt
Just (Right dp) -> formatUTCTime tz $ durationToEndUTC tz start dp
where
start = dtStartToUTC tz $ fromJust $ veDTStart e
durationToEndUTC :: TimeZone -> UTCTime -> DurationProp -> UTCTime
durationToEndUTC tz start (DurationProp duration _) = error "cannot handle DurationProp in DTEND"
formatUTCTime :: TimeZone -> UTCTime -> String
formatUTCTime tz dt = formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M")) $ utcToLocalTime tz dt
duration :: TimeZone -> VEvent -> String
duration tz e = printf "%.2f" (realToFrac (diffUTCTime end start) / 60 / 60 :: Float)
where
start = case veDTStart e of
Just dt -> dtStartToUTC tz dt
Nothing -> error "DTSTART is missing"
end = case veDTEndDuration e of
Just (Left dt) -> dtEndToUTC tz dt
Just (Right dp) -> durationToEndUTC tz start dp
Nothing -> oneHourFrom start -- instead of: error "DTEND is missing"
oneHourFrom :: UTCTime -> UTCTime
oneHourFrom = addUTCTime diff
where
diff = fromIntegral 3600 -- seconds
description :: String -> TimeZone -> VEvent -> String
description lineSep tz = maybe "" (eliminateLineBreaks lineSep . unpack . descriptionValue) . veDescription
summary :: String -> TimeZone -> VEvent -> String
summary lineSep tz = maybe "" (eliminateLineBreaks lineSep . unpack . summaryValue) . veSummary
location :: String -> TimeZone -> VEvent -> String
location lineSep tz = maybe "" (eliminateLineBreaks lineSep . unpack . locationValue) . veLocation
printError :: String -> IO ()
printError = hPutStrLn stderr
dtStartToUTC :: TimeZone -> DTStart -> UTCTime
dtStartToUTC tz (DTStartDateTime dt _) = dtToUTC tz dt
dtStartToUTC tz (DTStartDate d _) = dToUTC tz d
dtEndToUTC :: TimeZone -> DTEnd -> UTCTime
dtEndToUTC tz (DTEndDateTime dt _) = dtToUTC tz dt
dtEndToUTC tz (DTEndDate d _) = dToUTC tz d
dtToUTC :: TimeZone -> DateTime -> UTCTime
dtToUTC tz (FloatingDateTime localTime) = localTimeToUTC tz localTime
dtToUTC _ (UTCDateTime dt) = dt
dtToUTC _ (ZonedDateTime localTime tzTxt) = localTimeToUTC tz localTime
where tz = lookupTimeZone $ unpack tzTxt
-- Data.Time.Zones loadTZFromDB could do the job, but it's in the IO monad :(
lookupTimeZone :: String -> TimeZone
lookupTimeZone "Europe/Berlin" = hoursToTimeZone 1
lookupTimeZone s = error $ "TimeZone not implemented: " ++ s
dToUTC :: TimeZone -> Date -> UTCTime
dToUTC (TimeZone tzMinutes _ _) (Date day) = UTCTime day $ secondsToDiffTime offset
where offset = fromIntegral $ -1 * tzMinutes * 60
eliminateLineBreaks :: String -> String -> String
eliminateLineBreaks lineSep s = subRegex crnlRegex s lineSep
where
crnlRegex = mkRegex "\n\r?|\r\n?"