diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/lib/Options/Time.hs b/lib/Options/Time.hs
new file mode 100644
--- /dev/null
+++ b/lib/Options/Time.hs
@@ -0,0 +1,325 @@
+{-# LANGUAGE CPP #-}
+
+-- |
+-- Module: Options.Time
+-- License: MIT
+--
+-- The @options-time@ package provides 'OptionType' implementations and
+-- associated 'SimpleOption' instances for types related to dates and times.
+module Options.Time
+	( optionType_duration
+	, optionType_date
+	, optionType_time
+	, optionType_localTime
+	, optionType_utcTime
+	, optionType_zonedTime
+	) where
+
+import           Data.Fixed (divMod')
+import           Data.Ratio (numerator)
+import qualified Data.Time as T
+import           Options
+#if MIN_VERSION_time(1,3,0)
+import           Data.Time.LocalTime (makeTimeOfDayValid)
+#else
+import           Data.Fixed (Pico)
+#endif
+#if MIN_VERSION_time(1,5,0)
+import           Data.Time.Format (defaultTimeLocale)
+#else
+import           System.Locale (defaultTimeLocale)
+#endif
+import qualified Text.ParserCombinators.ReadP as R
+
+-- | Store an option as a 'T.DiffTime'. The duration is specified in a format
+-- such as @\"2h30m\"@ for two hours and thirty minutes.
+--
+-- Available units are:
+--
+-- * \"h\" for hours.
+-- * \"m\" for minutes.
+-- * \"s\" for seconds.
+-- * \"ms\" for milliseconds.
+-- * \"us\" or \"&#181;s\" for microseconds.
+-- * \"ns\" for nanoseconds.
+-- * \"ps\" for picoseconds.
+--
+-- Larger units are not available to avoid ambiguity when dealing with daylight
+-- savings time.
+optionType_duration :: OptionType T.DiffTime
+optionType_duration = optionType "duration" 0 parseDuration formatDuration
+
+instance SimpleOptionType T.DiffTime where
+	simpleOptionType = optionType_duration
+
+parseDuration :: String -> Either String T.DiffTime
+parseDuration = parsedOrErr where
+	parsedOrErr s = case R.readP_to_S parser s of
+		(duration,_):_ -> Right duration
+		[] -> Left (show s ++ " could not be parsed as a duration.")
+	parser = orderedChoice [zero, units]
+	zero = do
+		_ <- R.char '0'
+		R.eof
+		return (toPicoseconds 0)
+	units = do
+		minus <- R.option 1 (R.char '-' >> return (-1))
+		acc <- loop (toPicoseconds 0) 0
+		return (minus * acc)
+	loop acc dropIdx = do
+		digits <- R.munch1 (\c -> c >= '0' && c <= '9')
+		(multiplier, dropIdx')  <- orderedChoice (drop dropIdx parsers)
+		let acc' = acc + multiplier (read digits)
+		orderedChoice [done acc', loop acc' dropIdx']
+	done acc = R.eof >> return acc
+	parsers =
+		[ R.char 'h' >> return (toHours, 1)
+		, justMinuteSuffix >> return (toMinutes, 2)
+		, R.char 's' >> return (toSeconds, 3)
+		, R.string "ms" >> return (toMilliseconds, 4)
+		, R.string "us" >> return (toMicroseconds, 6)
+		, R.string "\181s" >> return (toMicroseconds, 6)
+		, R.string "ns" >> return (toNanoseconds, 7)
+		, R.string "ps" >> return (toPicoseconds, 8)
+		]
+	justMinuteSuffix = do
+		ahead <- R.look
+		case ahead of
+			'm':'s':_ -> R.pfail
+			_ -> R.char 'm'
+
+orderedChoice :: [R.ReadP a] -> R.ReadP a
+orderedChoice ps = case ps of
+	[] -> R.pfail
+	[p] -> p
+	(p:ps') -> p R.<++ orderedChoice ps'
+
+toPicoseconds :: Integer -> T.DiffTime
+toPicoseconds = T.picosecondsToDiffTime
+
+toNanoseconds :: Integer -> T.DiffTime
+toNanoseconds = toPicoseconds . (*1000)
+
+toMicroseconds :: Integer -> T.DiffTime
+toMicroseconds = toNanoseconds . (*1000)
+
+toMilliseconds :: Integer -> T.DiffTime
+toMilliseconds = toMicroseconds . (*1000)
+
+toSeconds :: Integer -> T.DiffTime
+toSeconds = T.secondsToDiffTime
+
+toMinutes :: Integer -> T.DiffTime
+toMinutes = toSeconds . (*60)
+
+toHours :: Integer -> T.DiffTime
+toHours = toMinutes . (*60)
+
+formatDuration :: T.DiffTime -> String
+formatDuration t = formatted where
+	formatted = if t == 0
+		then "0s"
+		else concat chunks
+	(negative, absolute) = if t < 0
+		then (True, t * (- 1))
+		else (False, t)
+	(rawSeconds, rawPicoFraction) = divMod' absolute 1 :: (Integer, T.DiffTime)
+	(hours, rawMinutes) = divMod rawSeconds 3600
+	(minutes, seconds) = divMod rawMinutes 60
+	rawPicos = numerator (toRational (rawPicoFraction*1000000000000))
+	(milliseconds, rawMicros) = divMod rawPicos 1000000000
+	(microseconds, rawNanos) = divMod rawMicros 1000000
+	(nanoseconds, picoseconds) = divMod rawNanos 1000
+	chunks =
+		[ if negative then "-" else ""
+		, chunk hours "h"
+		, chunk minutes "m"
+		, chunk seconds "s"
+		, chunk milliseconds "ms"
+		, chunk microseconds "us"
+		, chunk nanoseconds "ns"
+		, chunk picoseconds "ps"
+		]
+	chunk 0 _ = ""
+	chunk n suffix = show n ++ suffix
+
+-- | Store an option as a 'T.Day'. Supported formats are:
+--
+-- * \"YYYY-MM-DD\"
+-- * \"YYYYMMDD\"
+-- * \"YYYY-DDD\"
+optionType_date :: OptionType T.Day
+optionType_date = optionType "date" (T.fromGregorian 1970 1 1) parseDate formatDate
+
+instance SimpleOptionType T.Day where
+	simpleOptionType = optionType_date
+
+parseDate :: String -> Either String T.Day
+parseDate s = parsedOrErr where
+	parsedOrErr = case parsed of
+		Just day -> Right day
+		Nothing -> Left (show s ++ " could not be parsed as a date.")
+	parsed = firstJust
+		[ checkedParse "%Y-%m-%d" s
+		, checkedParse "%Y-%j" s
+		, checkedParse "%Y%m%d" s
+		]
+
+formatDate :: T.Day -> String
+formatDate = T.formatTime defaultTimeLocale "%Y-%m-%d"
+
+-- | Store an option as a 'T.TimeOfDay'. Supported formats are:
+--
+-- * \"HH:MM\"
+-- * \"HH:MM:SS\"
+-- * \"HH:MM:SS.FFFF\"
+--
+-- For example, the value @\"10:11:12.5\"@ is half a second past
+-- 10:11:12 AM.
+optionType_time :: OptionType T.TimeOfDay
+optionType_time = optionType "time" T.midnight parseTime formatTime
+
+instance SimpleOptionType T.TimeOfDay where
+	simpleOptionType = optionType_time
+
+parseTime :: String -> Either String T.TimeOfDay
+parseTime s = parsedOrErr where
+	parsedOrErr = case parsed >>= validateTime of
+		Just time -> Right time
+		Nothing -> Left (show s ++ " could not be parsed as a time.")
+	parsed = firstJust
+		[ checkedParse "%H:%M" s
+		, checkedParse "%H:%M:%S%Q" s
+		]
+
+formatTime :: T.TimeOfDay -> String
+formatTime = T.formatTime defaultTimeLocale "%H:%M:%S%Q"
+
+validateTime :: T.TimeOfDay -> Maybe T.TimeOfDay
+validateTime t = makeTimeOfDayValid (T.todHour t) (T.todMin t) (T.todSec t)
+
+#if !MIN_VERSION_time(1,3,0)
+-- Based on time-1.3:Data/Time/LocalTime/TimeOfDay.hs
+makeTimeOfDayValid :: Int -> Int -> Pico -> Maybe T.TimeOfDay
+makeTimeOfDayValid h m s = do
+	_ <- clipValid 0 24 h
+	_ <- clipValid 0 60 m
+	_ <- clipValid 0 61 s
+	return (T.TimeOfDay h m s)
+
+-- Based on time-1.3:Data/Time/Calendar/Private.hs
+clipValid :: (Ord t) => t -> t -> t -> Maybe t
+clipValid a _ x | x < a = Nothing
+clipValid _ b x | x >= b = Nothing
+clipValid _ _ x = Just x
+#endif
+
+-- | Store an option as a 'T.LocalTime'. Supported formats are a combination
+-- of those for 'optionType_date' and 'optionType_time'.
+optionType_localTime :: OptionType T.LocalTime
+optionType_localTime = optionType "local time" localEpoch parseLocalTime formatLocalTime
+
+instance SimpleOptionType T.LocalTime where
+	simpleOptionType = optionType_localTime
+
+localEpoch :: T.LocalTime
+localEpoch = T.LocalTime (T.fromGregorian 1970 1 1) T.midnight
+
+parseLocalTime :: String -> Either String T.LocalTime
+parseLocalTime s = parsedOrErr where
+	parsedOrErr = case parsed >>= validateLocalTime of
+		Just time -> Right time
+		Nothing -> Left (show s ++ " could not be parsed as a local time.")
+	parsed = firstJust $ do
+		ymd <- ["%Y-%m-%d", "%Y-%j", "%Y%m%d"]
+		hms <- ["%H:%M", "%H:%M:%S%Q", "%H%M%S%Q"]
+		sep <- [" ", "T"]
+		[checkedParse (ymd ++ sep ++ hms) s]
+
+formatLocalTime :: T.LocalTime -> String
+formatLocalTime = T.formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S%Q"
+
+validateLocalTime :: T.LocalTime -> Maybe T.LocalTime
+validateLocalTime t = do
+	_ <- validateTime (T.localTimeOfDay t)
+	return t
+
+-- | Store an option as a 'T.UTCTime'. Supported formats are a combination
+-- of those for 'optionType_date' and 'optionType_time'.
+optionType_utcTime :: OptionType T.UTCTime
+optionType_utcTime = optionType "utc time" utcEpoch parseUtcTime formatUtcTime
+
+instance SimpleOptionType T.UTCTime where
+	simpleOptionType = optionType_utcTime
+
+utcEpoch :: T.UTCTime
+utcEpoch = T.UTCTime (T.fromGregorian 1970 1 1) 0
+
+parseUtcTime :: String -> Either String T.UTCTime
+parseUtcTime s = parsedOrErr where
+	parsedOrErr = case parsed >>= validateUtcTime of
+		Just time -> Right time
+		Nothing -> Left (show s ++ " could not be parsed as a UTC time.")
+	parsed = firstJust $ do
+		ymd <- ["%Y-%m-%d", "%Y-%j", "%Y%m%d"]
+		hms <- ["%H:%M", "%H:%M:%S%Q", "%H%M%S%Q"]
+		sep <- [" ", "T"]
+		[checkedParse (ymd ++ sep ++ hms) s]
+
+formatUtcTime :: T.UTCTime -> String
+formatUtcTime = T.formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S%Q"
+
+validateUtcTime :: T.UTCTime -> Maybe T.UTCTime
+validateUtcTime t = case T.utctDayTime t of
+	x | x >= 0 && x < 86401 -> Just t
+	_ -> Nothing
+
+-- | Store an option as a 'T.ZonedTime'. Supported formats are a combination
+-- of those for 'optionType_date' and 'optionType_time'.
+optionType_zonedTime :: OptionType T.ZonedTime
+optionType_zonedTime = optionType "zoned time" zonedEpoch parseZonedTime formatZonedTime
+
+instance SimpleOptionType T.ZonedTime where
+	simpleOptionType = optionType_zonedTime
+
+zonedEpoch :: T.ZonedTime
+zonedEpoch = T.ZonedTime localEpoch T.utc
+
+parseZonedTime :: String -> Either String T.ZonedTime
+parseZonedTime s = parsedOrErr where
+	parsedOrErr = case parsed >>= validateZonedTime of
+		Just time -> Right time
+		Nothing -> Left (show s ++ " could not be parsed as a zoned time.")
+	parsed = firstJust $ do
+		ymd <- ["%Y-%m-%d", "%Y-%j", "%Y%m%d"]
+		hms <- ["%H:%M", "%H:%M:%S%Q", "%H%M%S%Q"]
+		sep <- [" ", "T"]
+		(tz, fixtz) <- [("", id), ("Z", setUTC), ("%z", id), (" %z", id), (" %Z", id)]
+		-- TODO: This doesn't support +01:00 because checkedParse will format
+		-- that to +0100 and fail.
+		[fixtz `fmap` checkedParse (ymd ++ sep ++ hms ++ tz) s]
+	setUTC t = t { T.zonedTimeZone = T.utc }
+
+formatZonedTime :: T.ZonedTime -> String
+formatZonedTime = T.formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S%Q %Z"
+
+validateZonedTime :: T.ZonedTime -> Maybe T.ZonedTime
+validateZonedTime t = do
+	_ <- validateLocalTime (T.zonedTimeToLocalTime t)
+	return t
+
+checkedParse :: (T.FormatTime t, T.ParseTime t) => String -> String -> Maybe t
+checkedParse fmt input = do
+	parsed <- T.parseTime defaultTimeLocale fmt input
+	-- Be fairly strict about the input format, because T.parseTime will
+	-- try to silently fix invalid inputs.
+	--
+	-- For example, "2014-20-12" is parsed as "2014-12-12".
+	if input == T.formatTime defaultTimeLocale fmt parsed
+		then Just parsed
+		else Nothing
+
+firstJust :: [Maybe a] -> Maybe a
+firstJust xs = case [x | Just x <- xs] of
+	x:_ -> Just x
+	[] -> Nothing
diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2012 John Millikin
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/options-time.cabal b/options-time.cabal
new file mode 100644
--- /dev/null
+++ b/options-time.cabal
@@ -0,0 +1,60 @@
+name: options-time
+version: 1.0
+license: MIT
+license-file: license.txt
+author: John Millikin <john@john-millikin.com>
+maintainer: John Millikin <john@john-millikin.com>
+build-type: Simple
+cabal-version: >= 1.8
+category: Console
+stability: stable
+homepage: https://john-millikin.com/software/haskell-options/
+
+synopsis: Command-line option types for dates and times.
+
+source-repository head
+  type: git
+  location: https://john-millikin.com/code/haskell-options-time/
+
+source-repository this
+  type: git
+  location: https://john-millikin.com/code/haskell-options-time/
+  tag: haskell-options-time_1.0
+
+flag old-locale
+
+library
+  ghc-options: -Wall -fno-warn-orphans
+  hs-source-dirs: lib
+
+  if impl(ghc > 7.4)
+    ghc-options: -fwarn-unsafe
+
+  build-depends:
+      base >= 4.1 && < 5.0
+    , options >= 1.0 && < 2.0
+
+  if flag(old-locale)
+    build-depends:
+        time >= 1.1.3 && < 1.5
+      , old-locale >= 1.0 && < 2.0
+  else
+    build-depends:
+        time >= 1.5 && < 2.0
+
+  exposed-modules:
+    Options.Time
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  main-is: Tests.hs
+
+  ghc-options: -Wall
+  hs-source-dirs: tests
+
+  build-depends:
+      base >= 4.0 && < 5.0
+    , chell >= 0.4 && < 0.5
+    , options >= 1.0 && < 2.0
+    , options-time
+    , time >= 1.1.3 && < 2.0
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,193 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+module Main (main) where
+
+import qualified Data.Time as T
+import           Options
+import           Options.Time
+import           Test.Chell
+
+test_Duration :: Test
+test_Duration = assertions "duration" $ do
+	let parse = optionTypeParse optionType_duration
+	let format = optionTypeShow optionType_duration
+	$expect $ equal
+		(parse "10h9m8s7ms6us5ns4ps")
+		(Right (sum
+			[ 10*60*60
+			, 9*60
+			, 8
+			, 7*0.001
+			, 6*0.000001
+			, 5*0.000000001
+			, 4*0.000000000001
+			]))
+	$expect $ equal (parse "1h9s") (Right 3609)
+	$expect $ equal (parse "1h9ms") (Right 3600.009)
+	$expect $ equal (parse "0") (Right 0)
+	$expect $ equal (parse "-1h9s") (Right (-3609))
+	$expect $ equal (parse "1s2us3ns") (parse "1s2\181s3ns")
+	$expect $ equal
+		(parse "bogus")
+		(Left "\"bogus\" could not be parsed as a duration.")
+	$expect $ equal
+		(parse "1ps2ps")
+		(Left "\"1ps2ps\" could not be parsed as a duration.")
+	$expect $ equal
+		(format (sum
+			[ 10*60*60
+			, 9*60
+			, 8
+			, 7*0.001
+			, 6*0.000001
+			, 5*0.000000001
+			, 4*0.000000000001
+			]))
+		"10h9m8s7ms6us5ns4ps"
+	$expect $ equal (format 0) "0s"
+	$expect $ equal (format 3609) "1h9s"
+	$expect $ equal (format (-3609)) "-1h9s"
+
+test_Date :: Test
+test_Date = assertions "date" $ do
+	let parse = optionTypeParse optionType_date
+	let format = optionTypeShow optionType_date
+	$expect $ equal
+		(parse "2011-12-13")
+		(Right (T.fromGregorian 2011 12 13))
+	$expect $ equal
+		(parse "20111213")
+		(Right (T.fromGregorian 2011 12 13))
+	$expect $ equal
+		(parse "2011-347")
+		(Right (T.fromGregorian 2011 12 13))
+	$expect $ equal
+		(parse "bogus")
+		(Left "\"bogus\" could not be parsed as a date.")
+	$expect $ equal
+		(parse "2011-10-32")
+		(Left "\"2011-10-32\" could not be parsed as a date.")
+	$expect $ equal (format (T.fromGregorian 2011 12 13)) "2011-12-13"
+
+test_Time :: Test
+test_Time = assertions "time" $ do
+	let parse = optionTypeParse optionType_time
+	let format = optionTypeShow optionType_time
+	$expect $ equal
+		(parse "10:11")
+		(Right (T.TimeOfDay 10 11 0))
+	$expect $ equal
+		(parse "20:21")
+		(Right (T.TimeOfDay 20 21 0))
+	$expect $ equal
+		(parse "20:21:22")
+		(Right (T.TimeOfDay 20 21 22))
+	$expect $ equal
+		(parse "20:21:22.232425")
+		(Right (T.TimeOfDay 20 21 22.232425))
+	$expect $ equal
+		(parse "bogus")
+		(Left "\"bogus\" could not be parsed as a time.")
+	$expect $ equal
+		(parse "25:21:00")
+		(Left "\"25:21:00\" could not be parsed as a time.")
+	$expect $ equal
+		(parse "20:21:60")
+		(Right (T.TimeOfDay 20 21 60))
+	$expect $ equal
+		(parse "20:21:61")
+		(Left "\"20:21:61\" could not be parsed as a time.")
+	$expect $ equal (format (T.TimeOfDay 20 21 0)) "20:21:00"
+	$expect $ equal (format (T.TimeOfDay 20 21 22.232425)) "20:21:22.232425"
+
+test_LocalTime :: Test
+test_LocalTime = assertions "local time" $ do
+	let parse = optionTypeParse optionType_localTime
+	let format = optionTypeShow optionType_localTime
+	let lt y mo d h mi s = T.LocalTime (T.fromGregorian y mo d) (T.TimeOfDay h mi s)
+	let rlt y mo d h mi s = Right (lt y mo d h mi s)
+	$expect $ equal (parse "2011-12-13 14:15") (rlt 2011 12 13 14 15 0)
+	$expect $ equal (parse "2011-12-13 14:15:16") (rlt 2011 12 13 14 15 16)
+	$expect $ equal (parse "2011-12-13 14:15:16.1718") (rlt 2011 12 13 14 15 16.1718)
+	$expect $ equal (parse "2011-347 14:15") (rlt 2011 12 13 14 15 0)
+	$expect $ equal (parse "20111213 14:15") (rlt 2011 12 13 14 15 0)
+	$expect $ equal (parse "20111213T141516") (rlt 2011 12 13 14 15 16)
+	$expect $ equal (parse "20111213T141516.1718") (rlt 2011 12 13 14 15 16.1718)
+	$expect $ equal
+		(parse "bogus")
+		(Left "\"bogus\" could not be parsed as a local time.")
+	$expect $ equal (format (lt 2011 12 13 14 15 0)) "2011-12-13 14:15:00"
+	$expect $ equal (format (lt 2011 12 13 14 15 16)) "2011-12-13 14:15:16"
+	$expect $ equal (format (lt 2011 12 13 14 15 16.1718)) "2011-12-13 14:15:16.1718"
+
+test_UtcTime :: Test
+test_UtcTime = assertions "utc time" $ do
+	let parse = optionTypeParse optionType_utcTime
+	let format = optionTypeShow optionType_utcTime
+	let ut y mo d h mi s = T.UTCTime (T.fromGregorian y mo d) (h*60*60 + mi*60 + s)
+	let rut y mo d h mi s = Right (ut y mo d h mi s)
+	$expect $ equal (parse "2011-12-13 14:15") (rut 2011 12 13 14 15 0)
+	$expect $ equal (parse "2011-12-13 14:15:16") (rut 2011 12 13 14 15 16)
+	$expect $ equal (parse "2011-12-13 14:15:16.1718") (rut 2011 12 13 14 15 16.1718)
+	$expect $ equal (parse "2011-347 14:15") (rut 2011 12 13 14 15 0)
+	$expect $ equal (parse "20111213 14:15") (rut 2011 12 13 14 15 0)
+	$expect $ equal (parse "20111213T141516") (rut 2011 12 13 14 15 16)
+	$expect $ equal (parse "20111213T141516.1718") (rut 2011 12 13 14 15 16.1718)
+	$expect $ equal (parse "2011-12-13 23:59:60") (rut 2011 12 13 23 59 60)
+	$expect $ equal
+		(parse "bogus")
+		(Left "\"bogus\" could not be parsed as a UTC time.")
+	$expect $ equal
+		(parse "2011-12-13 23:59:61")
+		(Left "\"2011-12-13 23:59:61\" could not be parsed as a UTC time.")
+	$expect $ equal (format (ut 2011 12 13 14 15 0)) "2011-12-13 14:15:00"
+	$expect $ equal (format (ut 2011 12 13 14 15 16)) "2011-12-13 14:15:16"
+	$expect $ equal (format (ut 2011 12 13 14 15 16.1718)) "2011-12-13 14:15:16.1718"
+
+test_ZonedTime :: Test
+test_ZonedTime = assertions "zoned time" $ do
+	let parse = optionTypeParse optionType_zonedTime
+	let format = optionTypeShow optionType_zonedTime
+	let zt y mo d h mi s tz = T.ZonedTime (T.LocalTime (T.fromGregorian y mo d) (T.TimeOfDay h mi s)) tz
+	let rzt y mo d h mi s tz = Right (zt y mo d h mi s tz)
+	let noZone = T.TimeZone 0 False ""
+	let tzPlus60 = T.TimeZone 60 False ""
+	let tzMinus60 = T.TimeZone (-60) False ""
+	let tzABC = T.TimeZone 0 False "ABC"
+	$expect $ equal (parse "2011-12-13 14:15") (rzt 2011 12 13 14 15 0 noZone)
+	$expect $ equal (parse "2011-12-13 14:15Z") (rzt 2011 12 13 14 15 0 T.utc)
+	$expect $ equal (parse "2011-12-13 14:15+0100") (rzt 2011 12 13 14 15 0 tzPlus60)
+	$expect $ equal (parse "2011-12-13 14:15 +0100") (rzt 2011 12 13 14 15 0 tzPlus60)
+	$expect $ equal (parse "2011-12-13 14:15-0100") (rzt 2011 12 13 14 15 0 tzMinus60)
+	$expect $ equal (parse "2011-12-13 14:15 -0100") (rzt 2011 12 13 14 15 0 tzMinus60)
+	$expect $ equal (parse "2011-12-13 14:15 ABC") (rzt 2011 12 13 14 15 0 tzABC)
+	$expect $ equal (parse "2011-12-13 14:15") (rzt 2011 12 13 14 15 0 noZone)
+	$expect $ equal (parse "2011-12-13 14:15:16") (rzt 2011 12 13 14 15 16 noZone)
+	$expect $ equal (parse "2011-12-13 14:15:16.1718") (rzt 2011 12 13 14 15 16.1718 noZone)
+	$expect $ equal (parse "2011-347 14:15") (rzt 2011 12 13 14 15 0 noZone)
+	$expect $ equal (parse "20111213 14:15") (rzt 2011 12 13 14 15 0 noZone)
+	$expect $ equal (parse "20111213T141516") (rzt 2011 12 13 14 15 16 noZone)
+	$expect $ equal (parse "20111213T141516.1718") (rzt 2011 12 13 14 15 16.1718 noZone)
+	$expect $ equal
+		(parse "bogus")
+		(Left "\"bogus\" could not be parsed as a zoned time.")
+	$expect $ equal (format (zt 2011 12 13 14 15 0 noZone)) "2011-12-13 14:15:00 +0000"
+	$expect $ equal (format (zt 2011 12 13 14 15 0 T.utc)) "2011-12-13 14:15:00 UTC"
+	$expect $ equal (format (zt 2011 12 13 14 15 0 tzPlus60)) "2011-12-13 14:15:00 +0100"
+	$expect $ equal (format (zt 2011 12 13 14 15 0 tzMinus60)) "2011-12-13 14:15:00 -0100"
+	$expect $ equal (format (zt 2011 12 13 14 15 16 noZone)) "2011-12-13 14:15:16 +0000"
+	$expect $ equal (format (zt 2011 12 13 14 15 16.1718 noZone)) "2011-12-13 14:15:16.1718 +0000"
+
+instance Eq T.ZonedTime where
+	x == y = key x == key y where
+		key t = (T.zonedTimeToLocalTime t, T.zonedTimeZone t)
+
+main :: IO ()
+main = Test.Chell.defaultMain [suite "tests"
+	[ test_Duration
+	, test_Date
+	, test_Time
+	, test_LocalTime
+	, test_UtcTime
+	, test_ZonedTime
+	]]
