timezone-detect 0.2.2.0 → 0.3.0.0
raw patch · 6 files changed
+197/−87 lines, 6 files
Files
- ChangeLog.md +12/−0
- README.md +52/−4
- src/Data/Time/LocalTime/TimeZone/Detect.hs +71/−22
- src/Foreign/ZoneDetect.hsc +6/−17
- test/TimezoneDetectSpec.hs +54/−42
- timezone-detect.cabal +2/−2
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