I'm attaching dtmconv.hs.
With this, you should be able to use the command in the example to
duplicate.
-- John
-- arch-tag: DTM conversion program
{-
TODO: categories
CHECK: can rid be eliminated? (palm uses it, so it doesn't seem to harm
anything)
Copyright (c) 2005 John Goerzen
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-}
import Text.XML.HaXml
import System.Posix.Time(epochTime)
import System.Time
import Text.Regex
import Data.List
import System.IO.Unsafe
-- Get an attribute value from an element.
attrofelem :: String -> Content -> AttValue
attrofelem attrname (CElem (Elem name al _)) =
case lookup attrname al of
Just x -> x
Nothing -> error $ "attrofelem: no " ++ attrname ++ " in " ++ name
attrofelem _ _ =
error "attrofelem: called on something other than a CElem"
--Render an attribute value as a string.
showattv :: AttValue -> String
showattv (AttValue v) = worker v
where worker [] = []
worker (Left x:xs) = x ++ worker xs
worker (Right x:xs) = worker xs
-- Parse stdin.
parse :: IO Content
parse =
do c <- getContents
return $ getContent $ xmlParse "(stdin)" c
where getContent (Document _ _ e) = CElem e
-- Render a Cnotent.
xml2str :: [Content] -> String
xml2str =
render . ppContent
where
ppContent [CElem e] = element e
ppContent [] = error "produced no output"
ppContent _ = error "produced more than one output"
-- Split a date. Returns Just (date, time) or Nothing if the input
-- was NULL or otherwise unparsable.
splitdate :: String -> Maybe (String, String)
splitdate x =
case break (== 'T') x of
(_, "") -> Nothing
(date, time) -> Just (date, tail time)
-- Convert a tag to calendar time.
tag2ct :: String -> Content -> Maybe CalendarTime
tag2ct x y = date2ct $ strof x y
-- Convert CT to epoch time.
ct2epoch :: CalendarTime -> Maybe Integer
ct2epoch ct =
if ctYear ct < 1971 || ctHour ct > 24 then Nothing
else case toClockTime ct of
TOD x _ -> Just x
-- Convert a date to a generic calendar time object.
-- Direct conversion. Must adjust tz in calendar time object if necessary.
date2ct :: String -> Maybe CalendarTime
date2ct d =
case matchRegexAll dregex d of
Just (_, _, _, [year, month, day, hour, min, sec]) ->
Just $ CalendarTime
{ctYear = read year,
ctMonth = toEnum ((read month) - 1),
ctDay = read day,
ctHour = read hour,
ctMin = read min,
ctSec = read sec,
ctPicosec = 0,
ctWDay = Sunday,
ctYDay = 0,
ctTZName = "",
ctTZ = 0,
ctIsDST = False}
Nothing -> Nothing
Just (_, _, _, x) -> error $ "Strange result: " ++ (show x)
where
dregex = mkRegex
"^([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])"
-- Program entry point
main :: IO ()
main = do time <- epochTime
tzoffset <- getTZOffset
-- UIDs start from a negative timestamp and decrease from there
let uid = (fromIntegral time) * (-1)
doc <- parse
let (addressdata, lastrid, lastuid) = getAddresses uid doc
writeFile "addressbook.xml" (xml2str addressdata)
putStrLn $ "Wrote addressbook.xml, rid 1 to " ++ (show lastrid) ++
", uid " ++ (show uid) ++ " to " ++ (show lastuid)
let (tododata, lastuidtodo) = getTodos (lastuid - 1) doc
writeFile "todolist.xml" (xml2str tododata)
putStrLn $ "Wrote todolist.xml, uid " ++ (show (lastuid - 1)) ++
" to " ++ (show lastuidtodo)
let (dbdata, lastuiddb) = getDB tzoffset (lastuidtodo - 1) doc
writeFile "datebook.xml" (xml2str dbdata)
putStrLn $ "Wrote datebook.xml, uid " ++ (show (lastuidtodo - 1)) ++
" to " ++ (show lastuiddb)
putStrLn " *** Conversion completed successfully! ***"
where getTZOffset :: IO Int
getTZOffset = do t <- getClockTime
cal <- toCalendarTime t
return $ ctTZ cal
-- Finds the literal children of the named tag, and returns it/them
tagof :: String -> CFilter
tagof x = keep /> tag x /> txt
-- Retruns the literal string that tagof would fine
strof :: String -> Content -> String
strof x y = verbatim $ tagof x $ y
{- Takes a list of (OldName, NewName) pairs. Returns a list of (NewName,
CFilter) pairs that will yield the content from calling tagof on the oldname.
-}
mapattrs :: [(String, String)] -> Content -> [(String, CFilter)]
mapattrs [] _ = []
mapattrs (x:xs) doc =
case strof (fst x) doc of
"" -> mapattrs xs doc -- Omit this tag if the content is empty
_ -> ((snd x), tagof (fst x)) : mapattrs xs doc
{- Like HaXml's numbered function, but instead of starting with 1 and
incrementing by 1, takes a start and a next. -}
versanumbered :: (Enum a, Show a) => a -> a -> CFilter -> LabelFilter String
versanumbered start next f = zip (map show [start,next..]) . f
----------------------------------------------------------------------
-- TODO LIST
------------------------------------------------------------
getTodos :: Integer -> Content -> ([Content], Integer)
getTodos startuid doc =
(tasks `o` inputTop $ doc,
startuid - count)
where
-- The top-level of the input
inputTop :: CFilter
inputTop = tag "Tasks" `o` children `o` tag "DTM"
-- The top-level of the output
tasks :: CFilter
tasks = mkElem "Tasks"
[row_task `oo` task_attrs]
count = genericLength $ children `o` inputTop $ doc
-- Each row if the input
task_attrs :: LabelFilter String
task_attrs = versanumbered startuid (startuid - 1)
(tag "Task" `o` children)
-- Each row of the output
row_task :: String -> CFilter
row_task uid inp = mkElemAttr "Task" rowattrs [] inp
where
rowattrs = mapattrs todomap inp
++ [("Uid", literal uid),
("Completed",
if (strof "MARK" inp) == "0"
then literal "1"
else literal "0"
)]
++ case splitdate . strof "ETDY" $ inp of
Nothing -> []
Just (date, _) -> [("StartDate",
literal date)]
++ case splitdate . strof "FNDY" $ inp of
Nothing -> []
Just (date, _) -> [("CompletedDate",
literal date)]
++ case splitdate . strof "LTDY" $ inp of
Nothing -> [("HasDate", literal "0")]
Just (date, _) ->
[("HasDate", literal "1")
,("DateYear", literal year)
,("DateMonth", literal month)
,("DateDay", literal day)]
where (year, yr) = splitAt 4 date
(month, mr) = splitAt 2 yr
day = mr
todomap = [("TITL", "Summary")
,("MEM1", "Description")
,("PRTY", "Priority")
]
----------------------------------------------------------------------
-- ADDRESS BOOK
----------------------------------------------------------------------
-- Main address book processor
getAddresses :: Integer -> Content -> ([Content], Integer, Integer)
getAddresses startuid doc =
(addressbook `o` inputTop $ doc,
count,
startuid - count)
where
-- The <Contacts> tag -- top-level of the input
inputTop :: CFilter
inputTop = tag "Contacts" `o` children `o` tag "DTM"
-- AddressBook -- the top level of the output
addressbook :: CFilter
addressbook = mkElem "AddressBook"
[mkElem "RIDMax" [literal (show (count + 1))]
,mkElem "Groups" []
,mkElem "Contacts" [row_contact `oo` contact_attrs]
]
count = genericLength $ children `o` inputTop $ doc
-- Each row of the input
contact_attrs :: LabelFilter (String, String)
contact_attrs = numbered `x` versanumbered startuid (startuid - 1)
$ tag "Contact" `o` children
-- Each row of the output
row_contact :: (String, String) -> CFilter
row_contact (rid, uid) inp =
mkElemAttr "Contact" rowattrs [] inp
where rowattrs =
[("FileAs", \x -> if (strof "FULL" x) `elem` ["", ",",
", "]
then tagof "CPNY" x
else tagof "FULL" x)
,("rid", literal rid)
,("Uid", literal uid)
,("rinfo", literal "1")
] ++ mapattrs addrmap inp
-- The address mapping
addrmap :: [(String, String)]
addrmap = [("TITL", "Title"), ("FNME", "FirstName"),
("MNME", "MiddleName"), ("LNME", "LastName"),
("SUFX", "Suffix"),
--FileAs, Categories, UID handled earlier
("DMAL", "DefaultEmail"), ("MAL1", "Emails"),
("HSTR", "HomeStreet"), ("HCTY", "HomeCity"),
("HSTA", "HomeState"), ("HZIP", "HomeZip"),
("HCTR", "Homecountry"), ("TEL1", "HomePhone"),
("FAX1", "HomeFax"), ("CPS1", "HomeMobile"),
("HWEB", "HomeWebPage"), ("CPNY", "Company"),
("BSTR", "BusinessStreet"), ("BCTY", "BusinessCity"),
("BSTA", "BusinessState"), ("BZIP", "BusinessZip"),
("BCTR", "BusinessCountry"),("BWEB", "BusinessWebPage"),
("PSTN", "JobTitle"), ("SCTN", "Department"),
("OFCE", "Office"), ("TEL2", "BusinessPhone"),
("FAX2", "BusinessFax"), ("CPS2", "BusinessMobile"),
("BPGR", "BusinessPager"), ("PRFS", "Profession"),
("ASST", "Assistant"), ("MNGR", "Manager"),
("SPUS", "Spouse"), ("CLDR", "Children"),
("GNDR", "Gender"), ("BRTH", "Birthday"),
("ANIV", "Anniversary"), ("NCNM", "Nickname"),
("MEM1", "Notes")
]
----------------------------------------------------------------------
-- DATE BOOK
------------------------------------------------------------
-- Main date book processor
getDB :: Int -> Integer -> Content -> ([Content], Integer)
getDB tzoffset startuid doc =
(events `o` inputTop $ doc,
startuid - count)
where
-- Tag to calendar time, considering tz
tag2cttz :: String -> Content -> Maybe CalendarTime
tag2cttz x y = case tag2ct x y of
Just a -> Just $ a {ctTZ = tzoffset}
Nothing -> Nothing
-- The top-level of the input
inputTop :: CFilter
inputTop = tag "Events" `o` children `o` tag "DTM"
-- The top level of the output
events :: CFilter
events = mkElem "events"
[row_event `oo` event_attrs]
count = genericLength $ children `o` inputTop $ doc
-- Each row of the input
event_attrs :: LabelFilter String
event_attrs = versanumbered startuid (startuid - 1)
(filter corruptfilter . tag "Event" `o` children)
-- Filter out corrupt rows.
corruptfilter :: Content -> Bool
corruptfilter inp = if strof "ADAY" inp `elem` ["1", "0"]
then True
else False
-- Each row of the output
row_event :: String -> CFilter
row_event uid inp = mkElemAttr "event" rowattrs [] inp
where
rowattrs = (mapattrs eventmap inp) ++ customattrs
++ times inp
times :: Content -> [(String, CFilter)]
times inp = case strof "ADAY" inp of
"1" -> -- All-day item
[("type", literal "AllDay")] ++
case (do c <- tag2cttz "ALSD" inp
ct <- ct2epoch $
c {ctHour = 0, ctMin = 0, ctSec = 0}
return $ show ct
) of
Nothing -> []
Just x -> [("start", literal (show x))]
++
case (do c <- tag2cttz "ALED" inp
ct <- ct2epoch $
c {ctHour = 23, ctMin = 59, ctSec = 0}
return $ show ct
) of
Nothing -> []
Just x -> [("end", literal (show x))]
_ -> -- Non-all-day item
case (do c <- tag2ct "TIM1" inp
ct <- ct2epoch c
return $ show ct
) of
Nothing -> []
Just x -> [("start", literal x)]
++
case (do c <- tag2ct "TIM2" inp
ct <- ct2epoch c
return $ show ct
) of
Nothing -> []
Just x -> [("end", literal x)]
customattrs :: [(String, CFilter)]
customattrs =
[("uid", literal uid)]
eventmap = [("DSRP", "description"),
("PLCE", "location"),
("MEM1", "note")
]