packages feed

timezone-detect 0.2.2.0 → 0.3.0.0

raw patch · 6 files changed

+197/−87 lines, 6 files

Files

ChangeLog.md view
@@ -1,5 +1,17 @@ # Changelog for timezone-detect +## v0.3.0.0 (2020-09-02)++**Breaking Changes!**++* Introduce `openTimeZoneDatabase` and `closeTimeZoneDatabase` to hew closer to+  the underlying library's intended usage. And `withTimeZoneDatabase` to manage the+  opening and closing of the TZ file around an IO computation with it.+* Changes the signature of `lookupTimeZoneName` to take a timezone database, not+  a file, same with `timeAtPointToUTC`. Introduces `*FromFile` variants that+  work with the path to the DB file and manage the opening/closing.++ ## v0.2.2.0 (2020-08-30)  * Explicitly import `MonadFail` and `fail`; hide the `fail` from `Prelude`.
README.md view
@@ -9,27 +9,47 @@ ## Usage  You'll need timezone database files to work with this library, see instructions [in the original repository](https://github.com/BertoldVdb/ZoneDetect/tree/master/database).+A copy is provided in the `test` directory of this repository, but it's intentionally not bundled in the package. We make no guarantees of its correctness,+we recommend you use the original authors' files! ++### Timezone Name Lookup+ Once you have those files in hand, you'll be able to get a timezone from a given latitude and longitude:  ```haskell->>> lookupTimeZoneName "./test/tz_db/timezone21.bin" 40.7831 (-73.9712) :: Maybe TimeZoneName+>>> db <- openTimeZoneDatabase "./test/tz_db/timezone21.bin" +>>> let tz = lookupTimeZoneName db 40.7831 (-73.9712) :: Maybe TimeZoneName Just "America/New_York"+>>> closeTimeZoneDatabase db ``` -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.+You can use `withTimeZoneDatabase` to "bracket" access to the file (take care of opening and closing,)+but if all you want to do is do a one-off lookup, a convenience function that opens and closes the file when done+is also provided, specialized to `IO`: +```haskell+>>> tz <- lookupTimeZoneNameFromFile "./test/tz_db/timezone21.bin" 40.7831 (-73.9712)+"America/New_York"+```++### LocalTime to UTCTime conversion++Additionally, we 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+>> db <- openTimeZoneDatabase "./test/tz_db/timezone21.bin" >>> 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+>>> utcTime <- timeAtPointToUTC db 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+>>> utcTime <- timeAtPointToUTC db 40.7831 (-73.9712) localWinter 2019-07-25 04:30:00 UTC+>>> closeTimeZoneDatabase db ```  You can also opt to obtain the timezone name separately (if you wanted to isolate that as a failure scenario,)@@ -40,3 +60,31 @@ >>> utcTime <- timeInTimeZoneToUTC "America/New_York" localSummer 2019-07-25 04:30:00 UTC ```++## Copyright++This library is released under the GPL v2; but the license for the underlying C library bears the following copyright:++> Copyright (c) 2018, Bertold Van den Bergh (vandenbergh@bertold.org) +> All rights reserved.+> Redistribution and use in source and binary forms, with or without+> modification, are permitted provided that the following conditions are met:+>    * Redistributions of source code must retain the above copyright+>       notice, this list of conditions and the following disclaimer.+>    * Redistributions in binary form must reproduce the above copyright+>       notice, this list of conditions and the following disclaimer in the+>       documentation and/or other materials provided with the distribution.+>    * Neither the name of the author nor the+>       names of its contributors may be used to endorse or promote products+>       derived from this software without specific prior written permission.+>+> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+> DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTOR BE LIABLE FOR ANY+> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND+> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
src/Data/Time/LocalTime/TimeZone/Detect.hs view
@@ -12,19 +12,30 @@  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.++Functions to open, close, and work with an open database (from a valid file) are provided. -}  module Data.Time.LocalTime.TimeZone.Detect -    ( lookupTimeZoneName+    ( TimeZoneName+    , TimeZoneDatabase+    -- for managing the database info pointer:+    , openTimeZoneDatabase+    , closeTimeZoneDatabase+    , withTimeZoneDatabase+    -- timezone lookup:+    , lookupTimeZoneName+    , lookupTimeZoneNameFromFile+    -- (local time, place) -> UTC "instant"     , timeAtPointToUTC+    , timeAtPointToUTCFromFile+    -- (local time, timezone name) -> UTC "instant"     , timeInTimeZoneToUTC-    , getTimeZoneSeriesFromOlsonFileUNIX-    , TimeZoneName ) where  import Foreign.ZoneDetect import Foreign.C.String (peekCAString, withCAString)-import Foreign (nullPtr)+import Foreign (Ptr, nullPtr) import Data.Time import Data.Time.LocalTime.TimeZone.Olson import Data.Time.LocalTime.TimeZone.Series@@ -32,6 +43,7 @@ import Control.Monad.IO.Class (MonadIO(liftIO)) import Control.Monad.Fail (MonadFail, fail) import Prelude hiding (fail)+import Control.Exception (bracket)  -- | Alias for clarity, timezones are path-like strings that follow the IANA conventions -- documented here:@@ -40,31 +52,31 @@ -- <https://en.wikipedia.org/wiki/Tz_database#Names_of_time_zones> type TimeZoneName = FilePath --- | 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+-- | A reference to a timezone database.+type TimeZoneDatabase = Ptr ZoneDetectInfo  -- | 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+-- >>> db <- openTimeZoneDatabase "./test/tz_db/timezone21.bin" +-- >>> let tz = lookupTimeZoneName db 40.7831 (-73.9712) :: Maybe TimeZoneName -- Just "America/New_York"+-- >>> closeTimeZoneDatabase db -- --- 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 = +-- An invalid database pointer, or invalid coordinates, will cause the given monad to `fail`.+-- Note that we follow the original library's pattern of obtaining the `TimeZoneDatabase`+-- separately (which could prove advantageous in e.g a web server.) If you're doing+-- a one-off lookup, or are okay with the IO cost of opening the DB file, see+-- `lookupTimeZoneNameFromFile`.+lookupTimeZoneName :: MonadFail m => TimeZoneDatabase -> Double -> Double -> m TimeZoneName+lookupTimeZoneName database lat lng =     unsafePerformIO $ do-        zdPtr <- withCAString databaseLocation $ \dbl -> c_ZDOpenDatabase dbl-        if zdPtr == nullPtr then-            pure $ fail (databaseLocation ++ " is not a valid timezone database.")+        if database == nullPtr then+            pure $ fail "Invalid timezone database."         else do-            tzName <- c_ZDHelperSimpleLookupString zdPtr+            tzName <- c_ZDHelperSimpleLookupString database                                                    (realToFrac lat)                                                    (realToFrac lng)             if tzName == nullPtr then@@ -72,6 +84,11 @@             else                 peekCAString tzName >>= (pure . return) +-- | Same as `lookupTimeZoneName`, but takes the path to the database file and only works in `IO`.+lookupTimeZoneNameFromFile :: FilePath -> Double -> Double -> IO TimeZoneName+lookupTimeZoneNameFromFile databaseLocation lat lng =+    withTimeZoneDatabase databaseLocation+        (\db -> lookupTimeZoneName db lat lng)   -- | Given a timezone name (presumably obtained via `lookupTimeZoneName`,) -- and a reference time in `LocalTime`, find the UTC equivalent.@@ -85,7 +102,39 @@ -- 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+timeAtPointToUTC :: TimeZoneDatabase -> Double -> Double -> LocalTime -> IO UTCTime+timeAtPointToUTC database lat lng referenceTime = do+    tzName <- lookupTimeZoneName database lat lng     timeInTimeZoneToUTC tzName referenceTime++-- | Same as `timeAtPointToUTC`, but takes the path to the timezone database file+-- and takes care of opening and closing the file.+timeAtPointToUTCFromFile :: FilePath -> Double -> Double -> LocalTime -> IO UTCTime+timeAtPointToUTCFromFile databaseLocation lat lng referenceTime =+    withTimeZoneDatabase databaseLocation+        (\db -> timeAtPointToUTC db lat lng referenceTime) ++-- | 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++-- | Open a timezone database file and obtain a pointer to the database,+-- as interpreted by the underlying C code.+openTimeZoneDatabase :: FilePath -> IO TimeZoneDatabase+openTimeZoneDatabase databaseLocation = +    withCAString databaseLocation $ \dbl -> c_ZDOpenDatabase dbl++-- | Given a pointer to a timezone database, close any allocated resources.+closeTimeZoneDatabase :: TimeZoneDatabase -> IO ()+closeTimeZoneDatabase = c_ZDCloseDatabase++-- | Given a path to a timezone database file, and a computation to run with it,+-- takes care of opening the file, running the computation and then closing it.+withTimeZoneDatabase :: FilePath -> (TimeZoneDatabase -> IO a) -> IO a+withTimeZoneDatabase databaseLocation = +    bracket (openTimeZoneDatabase databaseLocation)+            (closeTimeZoneDatabase)
src/Foreign/ZoneDetect.hsc view
@@ -8,27 +8,16 @@  #include <zonedetect.h> --- https://github.com/BertoldVdb/ZoneDetect/blob/05567e367576d7f3efa00083b7661a01e43dc8ca/library/zonedetect.c#L61--- note that we define the non-Windows version. This library will not compile in Windows systems! (sorry, I just--- don't know enough Haskell CPP directive-fu yet!)+-- | Opaque pointer to the underlying C struct:+-- https://wiki.haskell.org/FFI_cook_book#Passing_opaque_structures.2Ftypes+-- https://github.com/BertoldVdb/ZoneDetect/blob/05567e367576d7f3efa00083b7661a01e43dc8ca/library/zonedetect.h data ZoneDetectInfo = ZoneDetectInfo-    { fd :: Int-    , length :: CInt -- as per: https://hackage.haskell.org/package/bindings-posix-1.2.4/docs/Bindings-Posix-Sys-Types.html-    , closeType :: Word8-    , mapping :: Ptr Word8-    , tableType :: Word8-    , version :: Word8-    , precision :: Word8-    , numFields :: Word8-    , notice :: CString-    , fieldNames :: Ptr CString-    , bboxOffset :: Word32-    , metadataOffset :: Word32-    , dataOffset :: Word32-    }  foreign import ccall unsafe "zonedetect.h ZDOpenDatabase"     c_ZDOpenDatabase :: CString -> IO (Ptr ZoneDetectInfo)++foreign import ccall unsafe "zonedetect.hs ZDCloseDatabase"+    c_ZDCloseDatabase :: Ptr ZoneDetectInfo -> IO ()  foreign import ccall unsafe "zonedetect.h ZDHelperSimpleLookupString"     c_ZDHelperSimpleLookupString :: Ptr ZoneDetectInfo -> CFloat -> CFloat -> IO CString
test/TimezoneDetectSpec.hs view
@@ -7,9 +7,6 @@ zoneFile :: FilePath zoneFile = "./test/tz_db/timezone21.bin" -lookupTimeZoneName' :: Double -> Double -> Maybe TimeZoneName-lookupTimeZoneName' = lookupTimeZoneName zoneFile- localTimeFromString :: String -> IO LocalTime localTimeFromString =      parseTimeM True defaultTimeLocale "%Y-%-m-%-d %T"@@ -18,51 +15,22 @@ utcFromString =     parseTimeM True defaultTimeLocale "%Y-%-m-%-d %T" -timeAtPointToUTC' :: Double -> Double -> LocalTime -> IO UTCTime-timeAtPointToUTC' = timeAtPointToUTC zoneFile- spec :: Spec spec = do-    describe "lookupTimeZoneName" $ do-        it "calculates coordinates for known locations" $ do-            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 = lookupTimeZoneName "bogus" 0.0 0.0-            wrong `shouldBe` Nothing -- invalid DB file--        it "returns an error value when given invalid coordinates" $ do-            let outOfThisWorld = lookupTimeZoneName' 40000000.7 (-73.97)-            outOfThisWorld `shouldBe` Nothing -- invalid coordinates+    -- Simplistic functions: only work in IO, need a path to the TZ file, manage resources on their own.+    describe "lookupTimeZoneNameFromFile" $ do+        it "doesn't need a timezone db, just a path to the file, to detect the timezone." $ do+            newYork <- lookupTimeZoneNameFromFile zoneFile 40.7831 (-73.9712)+            newYork `shouldBe` "America/New_York" -    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"+    describe "timeAtPointToUTCFromFile" $ do+        it "doesn't need a timezone db, just a path to the file, to detect the UTC instant in a point and time" $ do             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+            utcSummer   <- utcFromString "2019-08-25 04:30:00"+            atPointSummer <- timeAtPointToUTCFromFile zoneFile 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-+         describe "timeInTimeZoneToUTC" $ do         it "calculates a UTC instant given a timezone name" $ do             localWinter <- localTimeFromString "2019-12-25 00:30:00"@@ -78,3 +46,47 @@             atTZWinter `shouldBe` utcWinter             atTZSummer `shouldBe` utcSummer             atTZNoDST  `shouldBe` alwaysSummer++    -- More general functions: work with a reference to an open TZ DB. You can use `withTimeZoneDatabase`+    -- for resource management (opens the file, does the computation, closes.)+    around (withTimeZoneDatabase zoneFile) $ do+        describe "lookupTimeZoneName" $ do+            it "returns the expected timezones for known locations" $ \db -> do+                let newYork = lookupTimeZoneName db 40.7831 (-73.9712)+                    tegucigalpa = lookupTimeZoneName db 14.0650 (-87.1715)+                newYork `shouldBe` (Just "America/New_York")+                tegucigalpa `shouldBe` (Just "America/Tegucigalpa")++            it "returns an error value when given invalid coordinates" $ \db -> do+                let outOfThisWorld = lookupTimeZoneName db 40000000.7 (-73.97)+                outOfThisWorld `shouldBe` Nothing -- invalid coordinates++            it "returns an error value when given a bogus database" $ \_ -> do+                badDb <- openTimeZoneDatabase "bogus"+                let bogus = lookupTimeZoneName badDb 40.7831 (-73.9712)+                bogus `shouldBe` Nothing++        describe "timeAtPointToUTC" $ do+            it "calculates a UTC instant at a point in time and space in New York" $ \db -> 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 db 40.7831 (-73.9712) localWinter+                atPointSummer <- timeAtPointToUTC  db 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)" $ \db -> 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 db 14.0650 (-87.1715) localWinter+                atPointSummer <- timeAtPointToUTC db 14.0650 (-87.1715) localSummer++                atPointWinter `shouldBe` utcWinter+                atPointSummer `shouldBe` utcSummer
timezone-detect.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 03ea7e2e9760969e8b6c4e2a5f8cb5a17b448db1b6b3d5749e18cf3f7336553a+-- hash: cada8fe310ed1ed1c85911dc9e3b40c06e5e111b2984429f42aa5a762a956c9a  name:           timezone-detect-version:        0.2.2.0+version:        0.3.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, Time