Skip to content

Commit d48083c

Browse files
committed
csv: convert datetimes with time zones to local date, mostly (WIP)
Abandoned for now, because in a non-UTC timezone, dates with no time/timezone all get adjusted (they are assumed to be 00:00:00 UTC).
1 parent fc364cd commit d48083c

File tree

2 files changed

+25
-17
lines changed

2 files changed

+25
-17
lines changed

hledger-lib/Hledger/Read/CsvReader.hs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import qualified Data.Text.Encoding as T
6060
import qualified Data.Text.IO as T
6161
import qualified Data.Text.Lazy as TL
6262
import qualified Data.Text.Lazy.Builder as TB
63-
import Data.Time.Calendar (Day)
63+
import Data.Time (UTCTime, Day, localDay, utcToLocalTime, getCurrentTimeZone, LocalTime (LocalTime))
6464
import Data.Time.Format (parseTimeM, defaultTimeLocale)
6565
import Safe (atMay, headMay, lastMay, readDef, readMay)
6666
import System.Directory (doesFileExist)
@@ -78,6 +78,7 @@ import Text.Printf (printf)
7878
import Hledger.Data
7979
import Hledger.Utils
8080
import Hledger.Read.Common (aliasesFromOpts, Reader(..),InputOpts(..), amountp, statusp, genericSourcePos, journalFinalise )
81+
import Data.Time.LocalTime (TimeZone)
8182

8283
--- ** doctest setup
8384
-- $setup
@@ -741,6 +742,7 @@ readJournalFromCsv mrulesfile csvfile csvdata =
741742
-- let (headerlines, datalines) = identifyHeaderLines records
742743
-- mfieldnames = lastMay headerlines
743744

745+
tz <- getCurrentTimeZone
744746
let
745747
-- convert CSV records to transactions
746748
txns = dbg7 "csv txns" $ snd $ mapAccumL
@@ -750,7 +752,7 @@ readJournalFromCsv mrulesfile csvfile csvdata =
750752
line' = (mkPos . (+1) . unPos) line
751753
pos' = SourcePos name line' col
752754
in
753-
(pos, transactionFromCsvRecord pos' rules r)
755+
(pos, transactionFromCsvRecord pos' rules tz r)
754756
)
755757
(initialPos parsecfilename) records
756758

@@ -874,8 +876,8 @@ hledgerField = getEffectiveAssignment
874876
hledgerFieldValue :: CsvRules -> CsvRecord -> HledgerFieldName -> Maybe Text
875877
hledgerFieldValue rules record = fmap (renderTemplate rules record) . hledgerField rules record
876878

877-
transactionFromCsvRecord :: SourcePos -> CsvRules -> CsvRecord -> Transaction
878-
transactionFromCsvRecord sourcepos rules record = t
879+
transactionFromCsvRecord :: SourcePos -> CsvRules -> TimeZone -> CsvRecord -> Transaction
880+
transactionFromCsvRecord sourcepos rules tz record = t
879881
where
880882
----------------------------------------------------------------------
881883
-- 1. Define some helpers:
@@ -884,7 +886,7 @@ transactionFromCsvRecord sourcepos rules record = t
884886
-- ruleval = csvRuleValue rules record :: DirectiveName -> Maybe String
885887
field = hledgerField rules record :: HledgerFieldName -> Maybe FieldTemplate
886888
fieldval = hledgerFieldValue rules record :: HledgerFieldName -> Maybe Text
887-
parsedate = parseDateWithCustomOrDefaultFormats (rule "date-format")
889+
parsedate = parseDateWithCustomOrDefaultFormats tz (rule "date-format")
888890
mkdateerror datefield datevalue mdateformat = T.unpack $ T.unlines
889891
["error: could not parse \""<>datevalue<>"\" as a date using date format "
890892
<>maybe "\"YYYY/M/D\", \"YYYY-M-D\" or \"YYYY.M.D\"" (T.pack . show) mdateformat
@@ -1269,16 +1271,24 @@ csvFieldValue rules record fieldname = do
12691271
fieldvalue <- T.strip <$> atMay record (fieldindex-1)
12701272
return fieldvalue
12711273

1272-
-- | Parse the date string using the specified date-format, or if unspecified
1273-
-- the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD, leading
1274-
-- zeroes optional).
1275-
parseDateWithCustomOrDefaultFormats :: Maybe DateFormat -> Text -> Maybe Day
1276-
parseDateWithCustomOrDefaultFormats mformat s = asum $ map parsewith formats
1274+
-- | Parse a date from a date/datetime string using the specified strptime format,
1275+
-- or else try all the "simple date" formats (YYYY/MM/DD, YYYY-MM-DD, YYYY.MM.DD
1276+
-- with optional leading zeroes).
1277+
--
1278+
-- If the string includes time and time zone, the local date (in the provided
1279+
-- local time zone) will be returned. This could be a day earlier or later than
1280+
-- the one in the string.
1281+
parseDateWithCustomOrDefaultFormats :: TimeZone -> Maybe DateFormat -> Text -> Maybe Day
1282+
parseDateWithCustomOrDefaultFormats tz mformat s = do
1283+
ut <- asum $ map parsewith formats :: Maybe UTCTime
1284+
let lt = utcToLocalTime tz ut :: LocalTime
1285+
let ld = localDay lt :: Day
1286+
return ld
12771287
where
12781288
parsewith = flip (parseTimeM True defaultTimeLocale) (T.unpack s)
12791289
formats = map T.unpack $ maybe
1280-
["%Y/%-m/%-d"
1281-
,"%Y-%-m-%-d"
1290+
["%Y-%-m-%-d"
1291+
,"%Y/%-m/%-d"
12821292
,"%Y.%-m.%-d"
12831293
-- ,"%-m/%-d/%Y"
12841294
-- ,parseTime defaultTimeLocale "%Y/%m/%e" (take 5 s ++ "0" ++ drop 5 s)

hledger/hledger.m4.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3865,11 +3865,9 @@ date-format %-m/%-d/%Y %l:%M %p some other junk
38653865
For the supported strptime syntax, see:\
38663866
<https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:formatTime>
38673867

3868-
Note that although you can parse date-times which include a time zone,
3869-
that time zone is ignored; it will not change the date that is parsed.
3870-
This means when reading CSV data with times not in your local time zone,
3871-
dates can be "off by one".
3872-
3868+
Note: date-times which include a time zone, different from your own local time zone,
3869+
will usually be parsed as the correct date in your time zone; but in certain situations
3870+
with daylight savings, it's possible for the parsed date to be "off by one".
38733871

38743872
### `decimal-mark`
38753873

0 commit comments

Comments
 (0)