timezone-detect 0.1.0.0 → 0.2.0.0
raw patch · 6 files changed
+173/−67 lines, 6 filesdep +timedep +timezone-olsondep +timezone-seriesdep ~base
Dependencies added: time, timezone-olson, timezone-series
Dependency ranges changed: base
Files
- ChangeLog.md +13/−0
- README.md +19/−3
- src/Data/Time/LocalTime/TimeZone/Detect.hs +81/−0
- src/TimezoneDetect.hs +0/−47
- test/TimezoneDetectSpec.hs +49/−12
- timezone-detect.cabal +11/−5
ChangeLog.md view
@@ -1,5 +1,18 @@ # Changelog for timezone-detect +## v0.2.0.0 (2020-08-30)++* Introduces dependencies on `time`, `timezone-series` and `timezone-olson`.+* __Breaking change__: this library is now aware of `Data.Time`, `TimezoneName` has been changed+ to `TimeZoneName` for consistency, and the `Detect` module is now a submodule of `Data.Time.LocalTime.TimeZone`.+* The function to find a timezone name is now more general (instead of `Either`) expects an instance of `MonadFail`,+ like `parseTimeM` in `Data.Time` does, and is now named `lookupTimeZoneName` for clarity.+* Introduces `timeAtPointToUTC` to determine the UTC instant represented by a local time in a latitude+ and longitude: uses the timezone-series and timezone-olson packages to reflect any daylight savings+ or other historical circumstances that may affect the timezone offset for the timezone in effect+ around the given geographic point.++ ## v0.1.0.0 (2020-08-29) * Bundles the C code for [ZoneDetect](https://github.com/BertoldVdb/ZoneDetect)
README.md view
@@ -3,7 +3,8 @@  -Haskell bindings to the excellent [ZoneDetect](https://github.com/BertoldVdb/ZoneDetect) library.+Haskell bindings to the excellent [ZoneDetect](https://github.com/BertoldVdb/ZoneDetect) library, plus additional+UNIX-aware facilities to determine the UTC time of a given local time in a latitude and longitude. ## Usage @@ -12,6 +13,21 @@ Once you have those files in hand, you'll be able to get a timezone from a given latitude and longitude: ```haskell-> lookupTimezone "./test/tz_db/timezone21.bin" 40.7831 (-73.9712)-Right "America/New_York"+>>> lookupTimeZoneName "./test/tz_db/timezone21.bin" 40.7831 (-73.9712) :: Maybe TimeZoneName+Just "America/New_York"+```++Additionally, we now depend on the [timezone-series](https://hackage.haskell.org/package/timezone-series) and [timezone-olson](https://hackage.haskell.org/package/timezone-olson) packages to add awareness of `tz database` information.++With that, you can look up the UTC time at a point in time and space:++```haskell+>>> import Data.Time+>>> localWinter <- parseTimeM True defaultTimeLocale "%Y-%-m-%-d %T" "2019-12-25 00:30:00"+>>> utcTime <- timeAtPointToUTC "./test/tz_db/timezone21.bin" 40.7831 (-73.9712) localWinter+2019-12-25 05:30:00 UTC++>>> localSummer <- parseTimeM True defaultTimeLocale "%Y-%-m-%-d %T" "2019-07-25 00:30:00"+>>> utcTime <- timeAtPointToUTC "./test/tz_db/timezone21.bin" 40.7831 (-73.9712) localWinter+2019-12-25 04:30:00 UTC ```
+ src/Data/Time/LocalTime/TimeZone/Detect.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Module: Data.Time.LocalTime.TimeZone.Detect+Portability: POSIX++Exposes utilities derived from the excellent ZoneDetect library <https://github.com/BertoldVdb/ZoneDetect>.+To use this module, you need to obtain database files from the aforementioned library's server.++Additionally, if you have a local time, latitude and longitude, we use the timezone-series and timezone-olson+packages to help determine the equivalent UTC instant to that point in geography and time.++The only relevant binding to ZoneDetect is 'lookupTimeZoneName', richer information that plays neatly+with the notion of 'TimeZone' could be derived, but I didn't have a personal need for that yet.+-}++module Data.Time.LocalTime.TimeZone.Detect + ( lookupTimeZoneName+ , timeAtPointToUTC+ , getTimeZoneSeriesFromOlsonFileUNIX+ , TimeZoneName+) where++import Foreign.ZoneDetect+import Foreign.C.String (peekCAString, withCAString)+import Foreign (nullPtr)+import Data.Time+import Data.Time.LocalTime.TimeZone.Olson+import Data.Time.LocalTime.TimeZone.Series+import System.IO.Unsafe (unsafePerformIO)+import Control.Monad.IO.Class (MonadIO(liftIO))+++-- | Alias for clarity, timezones are short strings that follow the IANA conventions+-- documented here:+-- <https://data.iana.org/time-zones/tz-link.html>+type TimeZoneName = String++-- | Gets timezone info from the standard location in UNIX systems.+-- The name should be one of the standard tz database names, as returned+-- by `lookupTimeZoneName`.+-- See: <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>+getTimeZoneSeriesFromOlsonFileUNIX :: TimeZoneName -> IO TimeZoneSeries+getTimeZoneSeriesFromOlsonFileUNIX tzName =+ getTimeZoneSeriesFromOlsonFile $ "/usr/share/zoneinfo/" <> tzName++-- | Given a timezone database, latitude and longitude, try to determine the+-- timezone name. Follow the instructions in the C library's repository+-- to obtain timezone database files (<https://github.com/BertoldVdb/ZoneDetect/tree/05567e367576d7f3efa00083b7661a01e43dc8ca/database>)+-- Once in possesion of said files, the lookup looks as follows:+-- +-- >>> tz <- lookupTimeZoneName "./test/tz_db/timezone21.bin" 40.7831 (-73.9712) :: Maybe TimeZoneName+-- Just "America/New_York"+-- +-- An invalid database file, or invalid coordinates, will cause the given monad to `fail`.+lookupTimeZoneName :: MonadFail m => FilePath -> Double -> Double -> m TimeZoneName+lookupTimeZoneName databaseLocation lat lng = + unsafePerformIO $ do+ zdPtr <- withCAString databaseLocation $ \dbl -> c_ZDOpenDatabase dbl+ if zdPtr == nullPtr then+ pure $ fail (databaseLocation <> " is not a valid timezone database.")+ else do+ tzName <- c_ZDHelperSimpleLookupString zdPtr+ (realToFrac lat)+ (realToFrac lng)+ if tzName == nullPtr then+ pure $ fail "Invalid coordinates."+ else+ peekCAString tzName >>= (pure . return)+++-- | Given a timezone database, latitude, longitude and a local reference time, find the UTC Time+-- equivalent of that reference time in the given timezone. The reference time helps determine+-- which offset was in effect, since daylight savings, historical circumstances, political revisions+-- and other circumstances (documented in the olson tz database) may have been in effect+-- at that point in spacetime.+timeAtPointToUTC :: FilePath -> Double -> Double -> LocalTime -> IO UTCTime+timeAtPointToUTC databaseLocation lat lng referenceTime = do+ tzName <- lookupTimeZoneName databaseLocation lat lng+ tzSeries <- liftIO $ getTimeZoneSeriesFromOlsonFileUNIX tzName+ return $ localTimeToUTC' tzSeries referenceTime
− src/TimezoneDetect.hs
@@ -1,47 +0,0 @@-{-|-Module: TimezoneDetect-Portability: POSIX--Exposes utilities derived from the excellent ZoneDetect library <https://github.com/BertoldVdb/ZoneDetect>.-To use this module, you need to obtain database files from the aforementioned library's server.--Currently, only one function for looking up the name of a Timezone is provided, but the underlying-library also has richer functions for opening timezone database files, finding applicable zones, etc.--}--module TimezoneDetect where--import Foreign.ZoneDetect-import System.IO.Unsafe (unsafePerformIO)-import Foreign.C.String (peekCAString, withCAString)-import Foreign (nullPtr)---- | Alias for clarity, timezones are short strings that follow the IANA conventions--- documented here:--- https://data.iana.org/time-zones/tz-link.html-type TimezoneName = String---- | Given a timezone database, latitude and longitude, try to determine the--- timezone name. Follow the instructions in the C library's repository--- to obtain timezone database files (<https://github.com/BertoldVdb/ZoneDetect/tree/05567e367576d7f3efa00083b7661a01e43dc8ca/database>)--- Once in possesion of said files, the lookup looks as follows:--- --- >>> lookupTimezone "./test/tz_db/timezone21.bin" 40.7831 (-73.9712)--- Right "America/New_York"--- --- Failure conditions are: invalid database file, or invalid coordinates,--- both are returned as `Left` values.-lookupTimezone :: FilePath -> Float -> Float -> Either String TimezoneName-lookupTimezone databaseLocation lat lng =- unsafePerformIO $ do- zdPtr <- withCAString databaseLocation $ \dbl -> c_ZDOpenDatabase dbl- if zdPtr == nullPtr then- pure $ Left (databaseLocation <> " is not a valid timezone database.")- else do- tzName <- c_ZDHelperSimpleLookupString zdPtr- (realToFrac lat)- (realToFrac lng)- if tzName == nullPtr then- pure $ Left "Invalid coordinates."- else- peekCAString tzName >>= (pure . Right)
test/TimezoneDetectSpec.hs view
@@ -1,27 +1,64 @@ module TimezoneDetectSpec (spec) where -import TimezoneDetect+import Data.Time+import Data.Time.LocalTime.TimeZone.Detect import Test.Hspec zoneFile :: FilePath zoneFile = "./test/tz_db/timezone21.bin" -lookupTimezone' :: Float -> Float -> Either String TimezoneName-lookupTimezone' = lookupTimezone zoneFile+lookupTimeZoneName' :: Double -> Double -> Maybe TimeZoneName+lookupTimeZoneName' = lookupTimeZoneName zoneFile +localTimeFromString :: String -> IO LocalTime+localTimeFromString = + parseTimeM True defaultTimeLocale "%Y-%-m-%-d %T"++utcFromString :: String -> IO UTCTime+utcFromString =+ parseTimeM True defaultTimeLocale "%Y-%-m-%-d %T"++timeAtPointToUTC' :: Double -> Double -> LocalTime -> IO UTCTime+timeAtPointToUTC' = timeAtPointToUTC zoneFile+ spec :: Spec spec = do- describe "lookupTimezone" $ do+ describe "lookupTimeZoneName" $ do it "calculates coordinates for known locations" $ do- let newYork = lookupTimezone' 40.7831 (-73.9712)- let tegucigalpa = lookupTimezone' 14.0650 (-87.1715)- newYork `shouldBe` (Right "America/New_York")- tegucigalpa `shouldBe` (Right "America/Tegucigalpa")+ let newYork = lookupTimeZoneName' 40.7831 (-73.9712)+ let tegucigalpa = lookupTimeZoneName' 14.0650 (-87.1715)+ newYork `shouldBe` (Just "America/New_York")+ tegucigalpa `shouldBe` (Just "America/Tegucigalpa") it "returns an error value when given an invalid timezone file path" $ do- let wrong = lookupTimezone "bogus" 0.0 0.0- wrong `shouldBe` (Left "bogus is not a valid timezone database.")+ let wrong = lookupTimeZoneName "bogus" 0.0 0.0+ wrong `shouldBe` Nothing -- invalid DB file it "returns an error value when given invalid coordinates" $ do- let outOfThisWorld = lookupTimezone' 40000000.7 (-73.97)- outOfThisWorld `shouldBe` (Left "Invalid coordinates.")+ let outOfThisWorld = lookupTimeZoneName' 40000000.7 (-73.97)+ outOfThisWorld `shouldBe` Nothing -- invalid coordinates++ describe "timeAtPointToUTC" $ do+ it "calculates a UTC instant at a point in time and space in New York" $ do+ localWinter <- localTimeFromString "2019-12-25 00:30:00"+ localSummer <- localTimeFromString "2019-08-25 00:30:00"+ utcWinter <- utcFromString "2019-12-25 05:30:00"+ utcSummer <- utcFromString "2019-08-25 04:30:00"+ + atPointWinter <- timeAtPointToUTC' 40.7831 (-73.9712) localWinter+ atPointSummer <- timeAtPointToUTC' 40.7831 (-73.9712) localSummer++ atPointWinter `shouldBe` utcWinter+ atPointSummer `shouldBe` utcSummer++ it "calculates a UTC instant at a point in time and space in Tegucigalpa (no DST)" $ do+ localWinter <- localTimeFromString "2019-12-25 00:30:00"+ localSummer <- localTimeFromString "2019-08-25 00:30:00"+ utcWinter <- utcFromString "2019-12-25 06:30:00"+ utcSummer <- utcFromString "2019-08-25 06:30:00"+ + atPointWinter <- timeAtPointToUTC' 14.0650 (-87.1715) localWinter+ atPointSummer <- timeAtPointToUTC' 14.0650 (-87.1715) localSummer++ atPointWinter `shouldBe` utcWinter+ atPointSummer `shouldBe` utcSummer
timezone-detect.cabal view
@@ -4,13 +4,13 @@ -- -- see: https://github.com/sol/hpack ----- hash: 38d5e310ad4b828f71ddd087380182d12af2a28c1d0f6c071a85581c34f499fa+-- hash: 1a675074d30bc8115c4ff2f0c6faf8641fc1b56787c8f3966eccbb379bb6012e name: timezone-detect-version: 0.1.0.0-synopsis: Haskell bindings for the zone-detect C library+version: 0.2.0.0+synopsis: Haskell bindings for the zone-detect C library; plus tz-aware utils. description: Please see the README on GitHub at <https://github.com/lfborjas/timezone-detect#readme>-category: Data, Foreign+category: Data, Foreign, Time homepage: https://github.com/lfborjas/timezone-detect#readme bug-reports: https://github.com/lfborjas/timezone-detect/issues author: Luis Borjas Reyes@@ -28,8 +28,8 @@ library exposed-modules:+ Data.Time.LocalTime.TimeZone.Detect Foreign.ZoneDetect- TimezoneDetect other-modules: Paths_timezone_detect hs-source-dirs:@@ -43,6 +43,9 @@ csrc/zonedetect.c build-depends: base >=4.7 && <4.14+ , time >=1.9.1 && <=1.10+ , timezone-olson >=0.2.0 && <0.3+ , timezone-series >=0.1.0 && <0.2 default-language: Haskell2010 test-suite timezone-detect-test@@ -60,5 +63,8 @@ base >=4.7 && <4.14 , directory >=1.3 && <1.4 , hspec >=2.7 && <2.8+ , time >=1.9.1 && <=1.10 , timezone-detect+ , timezone-olson >=0.2.0 && <0.3+ , timezone-series >=0.1.0 && <0.2 default-language: Haskell2010