tc39 / proposal-temporal

Provides standard objects and functions for working with dates and times.
https://tc39.es/proposal-temporal/docs/
Other
3.33k stars 151 forks source link

what are the use-cases for CivilDate and CivilTime that couldn't be fulfilled by CivilDateTime? #51

Closed kaizhu256 closed 6 years ago

kaizhu256 commented 7 years ago

CivilDate and CivilTime both look like unnecessary over-engineering to me, when CivilDateTime can fulfill both.

ljharb commented 7 years ago

How would you mark a day without a specific time with a CivilDateTime?

kaizhu256 commented 7 years ago

why not set default value to 00:00:00?

ljharb commented 7 years ago

A date at time 00:00:00 is a valid time; that's not the same thing as a date without a time.

kaizhu256 commented 7 years ago

think of this from a practical developer perspective instead of a pedantic architectural one. do i really want the extra overhead in a project of maintaining 3 separate kinds of temporal objects (and needless extra over-engineering to convert/combine them), rather than just dealing with a single unified one?

i know python also has 3 types, but in practice, most users just stick with Datetime, and ignore the other two.

ljharb commented 7 years ago

That's a simplistic view; there are plenty of use cases for the other two.

Saying "overengineering" for something that's totally valid - even if it's not something you'd use - is a bit condescending. Can we keep things civil? (pun intended :-p)

maggiepint commented 7 years ago

I'm going to note that Brazil does DST transitions at Midnight, so 00:00 is not a valid time in all zones.

We frequently get requests to Moment.js to have representations of Date-only and time-only types to reduce ambiguity when serializing/deserializing. When you explicitly parse something into a date-only, it makes it very clear that there is no time component. This drastically reduces ambiguity in downstream code.

crashposition commented 6 years ago

A good use case for splitting these out is to better reason about the output of a temporal Math operation - see: http://momentjs.com/guides/#/lib-concepts/date-time-math/

kaizhu256 commented 6 years ago

lets say you have a booking app, and you want to book appointment by a calendar ui. say i'm in los angeles and want to book a hotel reservation in new york on oct 31. except if i checkin 11pm la time, the date's already nov. 1 in new york. the same goes for flights.

or i'm a site reliability engineer in sunnyvale and i want to query server incident logs for dates oct. 1-oct. 10 local-time at a datacenter in singapore : /

civildate is pointless if you want to display date info across different timezones, which gives it very limited use-cases. when you want to display date info, the question always arise - which timezone? and in almost every use-case, the correct one is to use datetime to account for timezone.

ljharb commented 6 years ago

That dates need a timezone does not in any way mean they also need a time.

kaizhu256 commented 6 years ago

@ljharb can you give a common use-case in web or nodejs app using CivilDate that couldn't be done with CivilDateTime or builtin-Date?

@crashposition why can't CivilDateTime have both date and time arithmetic like moment.js already does?

even simpler - why can't we just extend builtin-Date with .addDate, .addTime, .makeImmutable methods? then users won't have the confusion of picking among Date/CivilDateTime/CivilDate/CivilTime, and dealing with context-switching-hell among them.

crashposition commented 6 years ago

@kaizhu256 you can. The point I'm making is that there is a valid use case for splitting them as their Math operations are very different. Date Math (even in UTC) has a lot of side effects.

kaizhu256 commented 6 years ago

i see ... how about extending Date with .copyAndAddDate and .copyAndAddTime methods, to avoid side-effects?

i feel whatever warts and quirks Date has, using it is still less painful to the user than context-switching among Date and CivilObjects.

ljharb commented 6 years ago

@kaizhu256 Airbnb represents check-in days and check-out days for both homes and experiences with times when available, but without times when not. This causes complications given that, for example, 00:00:00 doesn't exist in Brazil on one day a year. There doesn't exist a primitive, short of CivilDate, that omits the time entirely. Needing a date without a time is a common use case across programming, which is why it's an available primitive in most languages - and hopefully soon, in JS too.

a) If you find context-switching painful, you can always choose not to use the new types. b) you don't have to context-switch: the idea is you'd never use Date objects ever again.

kaizhu256 commented 6 years ago

@ljharb the simplest solution for the airbnb engineer is to use the same business-logic with datetime for all cases (using date-arithmetic if necessary), and simply hide the time-info @ the presentation-layer when the time is unavailable or exceptional. your use-case does not justify the need for separate CivilDate.

in an ideal world having everyone use only CivilObjects would be nice, but you and i both know that never happens in real-world web-projects. developers currently have to context-switch between moment.js and Date-objects when dealing with databases and ui, even when they don't want to, and its going to be no different with CivilObjects.

ljharb commented 6 years ago

@kaizhu256 in no way is it a simple solution to "hide" the time info but still have it there, invisibly affecting all operations.

What I know, in fact, is that the "ideal world" happens all the time in web projects, primarily by embracing new patterns and using automated tools, like linters and test coverage, to enforce their use. I can configure an eslint rule right now to ban all use of Date if I want - no context switching needed - and I'll certainly do that the instant this proposal is available.

kaizhu256 commented 6 years ago

@ljharb can you be more specific on what additional operations would be affected? i feel confident they can be mitigated with a simple .ignoreTime flag attached to the object

ljharb commented 6 years ago

@kaizhu256 adding mutable state is a much more complex thing to do than adding a new primitive type; that's just a nonstarter.

kaizhu256 commented 6 years ago

@ljharb

datetime = {
    // immutable JSON-friendly datetime that can be easily passed between
    // frontend <-> backend <-> indexeddb
    datetime: "2017-11-01T00:00:00.000Z"
    // flag to ignore time-component in presentation or business-logic
    ignoreTime: true,
    preferredTimezoneForPresentationLayer: "AMT" // brazil
}

datetimeOneWeekLater = {
    // lets extend Date with date-arithmetic using moment.js-like arguments
    datetime: new Date(datetime).copyAndAddDate(1, 'week').toISOString(),
    ignoreTime: datetime.ignoreTime,
    preferredTimezoneForPresentationLayer: datetime.preferredTimezoneForPresentationLayer
}
kaizhu256 commented 6 years ago

oh correction - the date constructor implicitly assumes input is in localtime. so:

a) perhaps extend Date with .copyAndAddDate date-arithmetic method? b) perhaps extend Date with .createFromISOString static function?

foo = {
    // immutable JSON-friendly datetime that can be easily passed between
    // frontend <-> backend <-> indexeddb
    datetime: "2017-11-01T00:00:00.000Z",
    // flag to ignore time-component in presentation or business-logic
    ignoreTime: true,
    preferredTimezoneForPresentationLayer: "AMT" // brazil
}

fooOneWeekLater = {
    datetime: Date.creatFromISOString(foo.datetime)
        // lets extend Date with date-arithmetic using moment.js-like arguments
        .copyAndAddDate(1, 'week')
        .toISOString(),
    ignoreTime: foo.ignoreTime,
    preferredTimezoneForPresentationLayer: foo.preferredTimezoneForPresentationLayer
}
hollowdoor commented 6 years ago

I think the process of ignoring a time component increases conceptual complexity. Like in times before our digital age the usual was to have a calendar, and a clock. The two rarely combined at all with a wrist watch here, and there that had a date viewer. People wearing watches with date views still used their paper calendars to mark future events.

I think we can learn from the less powerful analogue world where a distinction between calendars, and clocks is a distinction of modes. So here's some real world concepts related to time.

So clocks (time), and calendars have more obvious distinct modes in the analogue world.

Besides all that I think in a way date, and clock time are like multiplication, and addition. Multiplication is just another form of addition, and addition is a sub-form of multiplication. But we don't only have a single operator for both addition, and multiplication.

It would be very strange if we only had a multi/add operator that produced a result we had to parse to make it presentable. Distinct operators for different modes play well with set theory, and therefore play well with technically challenging solutions. Though we don't want the CivilSecond() constructor, but then we don't have a reason for that constructor while we have integers.

Though hybrids of calendar, and clock do exist I think these hybrids can also produce context switching because of things like "time ignoring" described here. And still having to rely on Date() in some situations is a regrettable historical artifact that could instead be remedied in "time" (at least for newly built websites).

kaizhu256 commented 6 years ago

hi @hollowdoor, can you translate what you just said into javascript-code for the airbnb use-case @ljharb presented? then we can objectively critique its engineering pros and cons against the other solutions provided.

hollowdoor commented 6 years ago

@kaizhu256 Some contrived examples. I omitted time zones as that's a different problem.

const tomorrow = new CivilDate(...);
//Somewhere pushing the checkout day forward
const later = tomorrow.plus({days: 2});
document.querySelector('#checkout-day').textContent = later; //No hiding, and so very easy

For when there is a possible date/time hybrid that needs to be converted to just a calendar date:

const dt = new CivilDateTime(...);
//The time is removed here, but tomorrow, and later are conceptually simpler
const tomorrow = new CivilDate(...).with(dt);
//Somewhere pushing the checkout day forward
const later = tomorrow.plus({days: 2});
document.querySelector('#checkout-day').textContent = later;

Or:

const tomorrow = new CivilDateTime(...);
//Somewhere pushing the checkout day forward
const later = tomorrow.plus({days: 2});
//Similar to hiding so not as good as the last example, but it is still easy
document.querySelector('#checkout-day').textContent = new CivilDate(...).with(later);

For when the clock time is sometimes needed:

const dt = new CivilDateTime(...);
//This condition is simpler than manual parsing
const tomorrow = showTime ? dt : new CivilDate(...).with(dt);
//Somewhere pushing the checkout day forward
const later = tomorrow.plus({days: 2});
document.querySelector('#checkout-day').textContent = later;

Using both calendar, and clock:

const dt = new CivilDateTime(...);
const later = (showTime ? dt : new CivilDate(...).with(dt)).plus({days: 2});
//Save the clock for later just in case
const clock = new CivilTime(...).with(dt);
document.querySelector('#checkout-day').textContent = later;
//Turns out we need the clock
document.querySelector('#event-time').textContent = clock.plus({hours: 14});

The idea is that conceptually a calendar without a clock component is more parsimonious than a hybrid calendar/clock because as a concept calendars have a different mode than clocks. So like in physical measurement systems we can be parsimonious by not converting between units unless we have to. Mostly because calendars, and clocks each in their different modes have subtle conflicts with each other.

The other basic idea is sometimes a person just wants a calendar, or a clock, and not both. As in analogue objects the digital world can benefit from this separation of two similar, but conceptually conflicting concepts.

As a tangential point I just noticed this standard could benefit from a .minus(date/time) operator.

kaizhu256 commented 6 years ago

@hollowdoor the more i look at use-cases, the more i'm convinced static-functions that manipulate and return immutable iso-strings are the best-course of action. doing so would give a unified programming-pattern for the use-cases you presented (instead of confusing the airbnb engineer with separate-code to deal with date and time individually)

// extend Date with static-functions to manipulate and return immutable iso-strings
Date.isoStringDatePlus = function (isoString, options) {...} // date-arithmetic like moment.js
Date.isoStringDatePlus('2017-11-14T00:00:00.000Z', {days: -1}); // '2017-11-13T00:00:00.000Z'

Date.isoStringTimePlus = function (isoString, options) {...} // time-arithmetic like moment.js
Date.isoStringTimePlus('2017-11-14T00:00:00.000Z', {seconds: 100}); // '2017-11-14T00:01:40.000Z'

Date.isoStringView = function (isoString, format) {...} // return a view of iso-string with given format
Date.isoStringView('2017-11-14T00:00:00.000Z', 'MM/DD/YYYY hh:mm:ss'); // '11/14/2017 00:00:00'

const dt = '2017-11-14T00:00.000Z';

//calculate tomorrow (unified approach for both date and datetime)
const tomorrow = Date.isoStringDatePlus(dt, {days: 1}); // '2017-11-15T00:00:00.000Z'

//Somewhere pushing the checkout day forward (unified approach for both date and datetime)
const later = Date.isoStringDatePlus(tomorrow, {days: 2}); // '2017-11-17T00:00:00.000Z'

//Turns out we need the clock
const clock = Date.isoStringTimePlus(later, {hours: 14, seconds: 100}); // '2017-11-18T02:01:40.000Z'

//show only date at presentation-level
document.querySelector('#checkout-day').textContent = Date.isoStringView(later, 'MM/DD/YYYY'); /// '11/17/2017'

//show hybrid (both date and time) at presentation-level
document.querySelector('#checkout-day').textContent = Date.isoStringView(later, 'MM/DD/YYYY hh:mm:ss'); // '11/17/2017 00:00:00'

// show clock
document.querySelector('#event-time').textContent = Date.isoStringView(clock, 'hh:mm:ss'); // '02:01:40'
crashposition commented 6 years ago

@hollowdoor I've been thinking along the same lines as "static-functions that manipulate and return immutable iso-strings". Internally use a tighter profile of ISO8601 (perhaps W3C-DTF) to store the canonical value but then pair all this with a broader Timestamp validation library.

kaizhu256 commented 6 years ago

@maggiepint i feel edge-cases like brazil's daylight savings where localtime 00:00:00 doesn't exist on 15 Oct 2017 are a presentation-layer concern, which is separate from utc-based business-logic.

these presentation issues can be handled by Date.isoStringView from my previous example:

// this function will return a view of isoString with the given timezone options for presentation
Date.isoStringView = function (isoString, options) {...}

Date.isoStringView('2017-10-15T04:00:00.000Z', {
    format: 'MM/DD/YYYY hh:mm:ss',
    timezone: 'AMST' // timezone-code for brazil's Amazon Summer Time
}); // '10/15/2017 01:00:00'

Date.isoStringView('2017-10-15T04:00:00.000Z', {
    format: 'MM/DD/YYYY hh:mm:ss',
    timezone: 'UTC-03:00:00' // timezone-code for brazil's Amazon Summer Time
}); // '10/15/2017 01:00:00'

Date.isoStringView('2017-10-15T04:00:00.000Z', {
    format: 'MM/DD/YYYY hh:mm:ss',
    timezone: 'localtime' // use local timezone of your machine
}); // '10/14/2017 21:00:00' - view of PDT time if your machine is set to los-angeles locale

// this function will convert localtime (from ui / inputs) to utc-based isoString (for business-logic)
Date.isoStringFromLocaltime = function (localtime, options) {...}

Date.isoStringFromLocaltime('2017-10-15T01:00:00.000Z', { timezone: 'AMST' })
// '2017-10-15T04:00:00.000Z'

Date.isoStringFromLocaltime('2017-10-15T01:00:00.000Z', { timezone: 'UTC-03:00:00' })
// '2017-10-15T04:00:00.000Z'

Date.isoStringFromLocaltime('2017-10-14T21:00:00.000Z', { timezone: 'localtime' })
// '2017-10-15T04:00:00.000Z' if machine set to los-angeles locale
ljharb commented 6 years ago

@kaizhu256 Edge cases like that have caused way way more bugs in production (in my personal experience alone; this isn't a grand claim I'm making) than any new language features ever have, and are absolutely not a "presentation layer" issue. That a period in time doesn't exist but can be represented by an alleged date-time object is hugely important to business logic.

hollowdoor commented 6 years ago

@kaizhu256 It's about calendar logic, and clock logic. Two constructors might be confusing to some people, but separate objects don't really seem all that strange to me. The two measurements are ratios of each other like Fahrenheit, and Celsius, or Imperial, and Metric. Keeping them separate as much as possible is more parsimonious. It's very maths oriented, and presentation oriented. The business, and the view can benefit from this.

kaizhu256 commented 6 years ago

@ljharb can you give use-case where business logic would be affected by unnecessary time-component, but couldn't be solved by masking it at presentation-level?

ljharb commented 6 years ago

@kaizhu256 very often at Airbnb, things like check-in dates, for example, are simply never presented, ie, are never used in a view, but are used in business logic to compute pricing information. Don't forget that JS is not (has never been) just a browser language, it's also a server language.

kaizhu256 commented 6 years ago

@ljharb are you saying you have 2 separate business-logic for calculating pricing-info for checkin/checkout for 1) date-only, and 2) datetime?

in that case, its still simpler for the engineer to use a unified datetime object (instead of two), and have a flag to tell server to ignore time component in date-only price-calculations

// unified datetime object to calculate price for both date and datetime cases
unifiedDatetime = {
    datetime: '2017-11-15T08:47:47.462Z' // or new CivilDateTime('2017-11-15T08:47:47.462Z'),
    dateOnly: true // set to true to tell server to calculate price using only date-component
}

the obvious advantage is the engineer doesn't have to write two separate serializers/class-constructors/converters for both date and datetime when baton-passing between frontend <-> backend <-> database

ljharb commented 6 years ago

@kaizhu256 yes, I am saying that; and no, it's not simpler to use a single object to mean two things - using two objects to each mean one thing is far far simpler. "Writing a serializer" isn't a hard problem, "understanding whether a thing is being used in one of many contexts", however, is.

hollowdoor commented 6 years ago

@kaizhu256 Sometimes the baton is more like a sack of batons.

{
    [date()] : [
        time(),
        time(),
        time()
    ]
}

Which can impact things like public APIs, and server to server communication/services. For things like node, and electron where JSON like tree structures are most common there would be no end to the benefits of separate date/time.

maggiepint commented 6 years ago

Don't want to wade deeply into this at the moment, but for examples of many people asking for this functionality see: https://github.com/moment/moment/issues/3455 and linked issues.

On Wed, Nov 15, 2017 at 11:32 AM, Quentin Engles notifications@github.com wrote:

@kaizhu256 https://github.com/kaizhu256 Sometimes the baton is more like a sack of batons.

{ [date()] : [ time(), time(), time() ] }

Which can impact things like public APIs, and server to server communication/services. For things like node, and electron where JSON like tree structures are most common there would be no end to the benefits of separate date/time.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-temporal/issues/51#issuecomment-344702979, or mute the thread https://github.com/notifications/unsubscribe-auth/AFxi0q8QXUZfXF72CXx02yFoUmNLgoQpks5s2zxZgaJpZM4PfnXv .

simonbuchan commented 6 years ago

Note the same case could be made about merging zone-info into Civil* - for example where I had to bucket event timestamps that occurred on a date on a non-local or UTC timezone - or to represent a month or week (w/ configurable first day of week) etc...

Obviously that doesn't scale at all.

Personally I'd rather use something more like Noda Time (and presumably Joda Time, which it was based on), where you have a much larger set of rich, semantic types, like (Period.Between(startDate, endDate, PeriodUnits.Weeks) + Period.FromDays(2)).ToDuration().TotalHours and DateTimeZonesProvider.Tzdb.ForId("America/Sao_Paulo").AtStartOfDay(localDate), but speccing that out would be a nightmare.

littledan commented 6 years ago

I'm not sure if we should conclude on this issue yet. Could we release the polyfill to npm, see what people think, and make the decision on that basis?