Skip to content

Conversation

cooldome
Copy link
Member

After major rework of times module by GULF, I decided to give it a second chance and try to replace my hand written closed source Date module with standard lib times module.
I found that now times has all the functionality I need, but unfortunately external interface is such that I can't used it effectively. Coupling date and time together has its drawbacks. Time requires timezones and timezones requires reading external files that takes enormous amount time compared to rest of the calculations.

There no tests yet so it is work in progress, but I would like to start the discussion if anyone see major problems with the design.

Summary of changes:

  • New types: Date and EpochDate
  • DateTime is inherited from Date which helps sharing large part of the functionality.
  • Date has similar functionality to DateTime but much more efficient
  • weekday and yearday are now computed on the fly

@GULPF
Copy link
Member

GULPF commented Jan 17, 2018

Looks great! Have you seen #6978? I think it makes some changes that are relevant to this PR.

I think instead of introducing separate procs like parseDate, date we should use generics (with overloads that assume DateTime):

proc now[D: Date|DateTime](): D
proc parse[D: Date|DateTime](value, format: string): D

The reason that I prefer that design is that I think it will be more intuitive when even more types are added (TimeOfDay and NaiveDateTime).

DateTime is inherited from Date which helps sharing large part of the functionality.

I disagree with this. When TimeOfDay is introduced DateTime can't inherit from it, even though it makes as much sense as inheriting from Date. It's better to let DateTime contain a Date and use DateTime|Date to share functionality.

weekday and yearday are now computed on the fly

Excellent. This will break code that tries to modify those fields, but IMO it's worth it.

rtl, extern: "ntEqTime", tags: [], raises: [], noSideEffect, borrow.}
## Returns true if ``a == b``, that is if both times represent the same point in time.

proc `-`*(a, b: EpochDate): int32 {.borrow.}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should return the Duration type from #6978

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As soon as Duration will appear in devel I can do that, otherwise it is problematic to test

proc date*(monthday: MonthdayRange, month: Month, year: int): Date =
## Create a new ``Date``
result = Date(monthday: monthday, month: month, year: year)
assertValidDate result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proc should be named initDate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

proc date*(s: string): Date =
## Parse `s` date string using ISO format
runnableExamples:
date"2019-03-21"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example should verify the result of date"2019-03-21", this will likely complain about missing discard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure about this proc. If the use case is initialization, then IMO it should limit the argument to string literals so that parseDate(str, "yyyy-MM-dd") must be used when parsing input. A proc named date that does parsing seems a bit unclear.

Copy link
Member Author

@cooldome cooldome Jan 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this proc as requested. I can add date alias locally in my project.
I understand that it is deviation from the existing API.
However, it is super convenient to have a single function date that does all ways of date creation.

date"2019-02-03"
date(2019, mFeb, 3)
date(2019, 02, 03)
date() current date
date(x), where x can be EpochDate, Time, DateTime

So all you need to remember is the word date and you will be just fine with stdlib.
You can't do that effectively with DateTime as it has far too many members to keep specifying them one by one.

Please vote if you want date function back

proc toEpochDate*(time: Time): EpochDate =
EpochDate(time.int64 div secondsInDay)

proc toEpochDate*(epoch_days: int32): EpochDate =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be epochDays instead of epoch_days

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

let dt = date()
result.monthday = dt.monthday
result.month = dt.month
result.year = dt.year
Copy link
Member

@GULPF GULPF Jan 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct? It will overwrite all of monthday, month and year even if some of them might have been parsed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous behavior was to call now() at the begging of parse function unconditionally. It is tearing my heart apart when I see a such wasteful behavior, now() is not cheap and it is 95% probably that you will find a date in the string, so results of now() call will be overridden and wasted. The idea is to call now() only it date was not found in the string

assertValidDate d
toEpochDate(d.year, d.month.int, d.monthday.int)

proc date*(epochdate: EpochDate): Date =
Copy link
Member

@GULPF GULPF Jan 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike the naming of this. I think fromEpochDate better matches the rest of the API in the times module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done for now, unless idea of single entry date will be supported

proc yearday*(d: Date): YeardayRange {.tags: [], raises: [], benign .} =
## Returns the day of the year since January 1,
## in the range 0 to 365.
assertValidDate d
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertValidDate shouldn't be needed here. When taking proper types like Date or DateTime as input they can be assumed to be valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

proc toEpochDate*(epoch_days: int32): EpochDate =
epoch_days.EpochDate

proc toEpochDate(year, month, day: int): EpochDate =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the argument ordering day, month, year instead to be consistent with rest of the times module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@GULPF
Copy link
Member

GULPF commented Jan 17, 2018

After thinking about this some more, I don't think my idea of using generics for now/parse is good. parseX is how most parse procs are named in the stdlib (parseJson, parseInt etc) so parseDate proc is a good name. However, I still dislike the date proc which returns the current date. I'd rather add a proc for converting DateTime to Date so that now().date can be used instead.

@dom96
Copy link
Contributor

dom96 commented Jan 17, 2018

If you're using inheritance then I don't think generics are a good idea.

Why the need for EpochDate?

@cooldome
Copy link
Member Author

@GULPF
comment on

However, I still dislike the date proc which returns the current date. I'd rather add a proc for converting DateTime to Date so that now().date can be used instead"

now() is producing DateTime by converting Time to DateTime. Time->DateTime->Date is far to inefficient, it should be Time->Date . I have changed the name to nowDate to closer match existing API. I hope it is ok this way

@cooldome
Copy link
Member Author

@dom96

EpochDate naturally exists. It is in the heart of every date algorithm. The only question if you want to expose it or not.

For example how is any DateTime gets produced in reality. It is a sequence of conversions:
EpochTime -> EpochDate + remainder -> Date + remainder -> Date + Time -> DateTime
Step from remainder to Time requires timezone. I want user to be able to use parts of this pipeline if he wants to. I have concentrated on Date part, having time part separately would also help. Many date operations are more efficient on EpochDate then on Date. As a side note, I have a generic type in my code: Interval[a, b: static[EpochDate], T] = distinct T, given that EpochDate is as efficient as int, it helps compilation efficiency

let dt = nowDate()
result.monthday = dt.monthday
result.month = dt.month
result.year = dt.year
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take his example: parse(str, "MM/dd"). It should parse the month and monthday and use the current year. This code however will overwrite all of the date fields with the current ones. The code needs to check if each individual field is missing before overwritting:

if ptMonthday notin found_tokens:
  result.monthday = dt.monthday

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

$d.year & "-" & $ord(d.month) & "-" & $d.monthday & " is not a valid date"

proc toEpochDate*(time: Time): EpochDate =
EpochDate(time.int64 div secondsInDay)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will be correct for times before 1970-01-01, e.g a time of -1 will give the epoch date 0, but it should be -1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, should work now

@GULPF
Copy link
Member

GULPF commented Jan 21, 2018

now() is producing DateTime by converting Time to DateTime. Time->DateTime->Date is far to inefficient, it should be Time->Date . I have changed the name to nowDate to closer match existing API. I hope it is ok this way

The reason that nowDate is fast is because it doesn't consider timezones, but it should consider timezones. It would be very confusing if now() and nowDate() gives different dates. This also means that the change to getDateStr changes behavior: previously it returned the local date, now it returns the UTC date.

If we need a fast and short way to get the current UTC day, would introducing a nowUtc proc be enough, so that nowUtc().date can be used? Allowing getTime().toDate is another alternative.

If we really need a single proc for getting the UTC date, then it should be named nowDateUtc or similar, but I think it would be overkill. It would require us to have two nowX procs for every introduced type to be consistent, resulting in 8 different nowX procs when TimeOfDay and NaiveDateTime is introduced.

getTime().toEpochDate().fromEpochDate()

After reading this I think I was wrong to say that fromEpochDate is a good name for the proc, that code looks incredible confusing... I forgot that the new API have multiple variants of toEpochDate. The EpochDate->Date proc should probably just be called toDate. Sorry for the bikeshedding

@cooldome
Copy link
Member Author

@GULPF
Are you sure that timezone changes can cause date differences, not just time of the day differences?
It is news to me. Googled it quickly, can't find any evidence of that.

@GULPF
Copy link
Member

GULPF commented Jan 21, 2018

@cooldome
Yes, here is an example output of zdump that I just ran:

$ zdump Asia/Anadyr
Asia/Anadyr  Mon Jan 22 01:06:19 2018 +12
$ zdump Europe/Stockholm
Europe/Stockholm  Sun Jan 21 14:06:29 2018 CET

As you can see, the date in Asia/Anadyr is currently Jan 22, while in Europe/Stockholm it's currently Jan 21. What else would happen when for example UTC is at 23:00 and you're in a timezone that is more than one hour ahead of UTC?

@cooldome
Copy link
Member Author

cooldome commented Jan 21, 2018

@GULPF:
I have renamed fromEpochDate to toDate. toDate is definitely better.
Thanks for your patience and comprehensive description of the nowDate() pitfalls.
nowDate is now removed as it causes more problems then solves, those who want quick UTC date can use getTime.toEpochDate.toDate

@johannschopplich
Copy link

johannschopplich commented Feb 27, 2018

I catched myself in the act again checking for an update on this PR. I use the times module extensively, but need improvements. This is what I'm waiting for. Great work—can't wait for an update / merge on this. No joke.

@Araq
Copy link
Member

Araq commented Feb 27, 2018

@jschopplich Will have to a wait a little longer then, this won't make it into the next release, sorry.

@Araq
Copy link
Member

Araq commented Feb 27, 2018

nowDate is now removed as it causes more problems then solves, those who want quick UTC date can use getTime.toEpochDate.toDate

That sounds wrong. If so, why is nowDate not an alias for getTime.toEpochDate.toDate?

@GULPF
Copy link
Member

GULPF commented Feb 27, 2018

@Araq
Read the discussion above. getTime.toEpochDate.toDate uses UTC, but now() uses the local timezone. If there should be a nowDate, it should use the local timezone as well.

@johannschopplich
Copy link

johannschopplich commented Feb 27, 2018

Will have to a wait a little longer then, this won't make it into the next release, sorry.

Alright. 😐 A proper times module is the one thing missing to put Nim above other languages in my production use cases. I will wait then—thank you @GULPF and @cooldome for the already merged improvements since v0.17.2. Keep it up! 👍

@skilchen
Copy link
Contributor

Please don't introduce the inheritance relation between Date and DateTime.
They are simply different scales on an abstract notion of time (similar to km and m for distances, but nobody thinks that a meter inherits something from a kilometer). Do you know any other Date/Time library where DateTime inherits from Date?

Consider what happens if you want to implement arithmetic on Dates. Lets say you implement subtraction of date1 from date2. With your inheritance relation this becomes also valid for DateTimes but gives undesired results. How would you then implement subtraction specifically for DateTimes?

Given that @GULPF will surely convince you, that you have to take timezones into account also for dates, you actually gain very little from a separate Date type.

@GULPF
Copy link
Member

GULPF commented Apr 2, 2018

Given that @GULPF will surely convince you, that you have to take timezones into account also for dates, you actually gain very little from a separate Date type.

I'm not sure what you mean by this since the Date type doesn't have a timezone, but I agree that DateTime shouldn't inherit from Date.

@skilchen
Copy link
Contributor

skilchen commented Apr 3, 2018

Given that @GULPF will surely convince you, that you have to take timezones into account also for dates, you actually gain very little from a separate Date type.

sorry this was a failed attempt to argue from authority ...

if dates represent some number of days since an epoch and days are defined as in UTC (based on solar mean time), then it seems clear to me, that at least the question "what date is it today?" depends just as much on the location on earth where you are located as the question "what time is it now()?".

@GULPF
Copy link
Member

GULPF commented Apr 3, 2018

The question "what date is it today" requires knowledge of timezones, but not questions like "what date was I born?" or "what date is next Christmas?". The main purposes of the Date type is to track abstract dates that doesn't represent any specific point in time, which it can't do if it represents some number of days since an epoch.

The fact that this PR adds the type EpochDate muddies the waters a bit. I dislike the type because it's not clear what it represents. Is equivalent to Date but stored as an int32, or is it equivalent to Time but with days as resolution? If it was an internal type it wouldn't really matter, but I think as an exported type it's not clear enough what it actually represents.

@skilchen
Copy link
Contributor

skilchen commented Apr 6, 2018

my humble opinion: close this PR and wait until @GULPF comes up with a separate Date type

@Araq
Copy link
Member

Araq commented Apr 13, 2018

I think the PR queue for times.nim is now otherwise empty. Time to revisit this.

@cooldome
Copy link
Member Author

Was not paying attention what is was happening with this PR. Will get back to it soon

@Araq
Copy link
Member

Araq commented Aug 31, 2018

A friendly reminder. Ping @cooldome

@cooldome
Copy link
Member Author

cooldome commented Sep 7, 2018

ah, yes. In next release then, all my energy is drained by #8489 which a blocker for my project

@Araq
Copy link
Member

Araq commented Dec 21, 2018

No progress. Please reopen when you get back to it.

@timotheecour
Copy link
Member

timotheecour commented Dec 21, 2018

@Araq that's what the labelS: Waiting on author is for :-) (even if you close it, at least adding that label helps)
helps triaging since there's good stuff here that'd be a shame to loose in case either author or someone else wants to take over

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants