Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISOString with static-functions as alternative to CivilObjects #53

Closed
kaizhu256 opened this issue Nov 16, 2017 · 12 comments
Closed

ISOString with static-functions as alternative to CivilObjects #53

kaizhu256 opened this issue Nov 16, 2017 · 12 comments

Comments

@kaizhu256
Copy link
Contributor

kaizhu256 commented Nov 16, 2017

creating a separate issue from sub-topic that was discussed in #51

we mentioned extending Date with static-functions to manipulate string-representations of date/time/datetime (possibly following the https://www.w3.org/TR/NOTE-datetime standard) as alternative to CivilObjects.

pros:

  1. no need to spec out new data-types for user to learn (simply extend builtin Date with a few static-functions to manipulate strings)
  2. data being an ISOString is easily baton-passable between frontend <-> backend <-> database
  3. data being an ISOString is easy to debug/modify/parse with regexp and string functions.

cons:

  1. ???

the static-function equivalents for CivilObject methods could be as follow:

Date.isoStringDatePlus = function (isoDate, options) {
/*
 * this function will perform date-arithmetic on isoDate with the given options.
 */
    var isoDate2;
    // ...
    return isoDate2;
};

Date.isoStringTimePlus = function (isoTime, options) {
/*
 * this function will perform time-arithmetic on isoTime with the given options.
 */
    var isoTime2;
    // ...
    return isoTime2;
};

Date.isoStringDateTimePlus = function (isoDateTime, options) {
/*
 * this function will perform datetime-arithmetic on isoDateTime with the given options.
 */
    var isoDateTime2;
    // ...
    return isoDateTime2;
};

Date.isoStringInstantGetMilliseconds = function (isoDateTime) {
/*
 * this function will return the
 * integer value representing the number of milliseconds elapsed
 * from 1970-01-01 00:00:00.000 UTC, without regarding leap seconds
 */
    var milliseconds;
    milliseconds = new Date(isoDateTime);
    milliseconds = milliseconds.getTime() - 60 * 1000 * milliseconds.getTimezoneOffset();
    return milliseconds;
};

Date.isoStringZonedInstantView = function (isoDateTime, options) {
/*
 * this function will return a view of isoDateTime with the given options
 */
    var view, template, utcToZone, zoneToUtc;
    template = options.template; // e.g. '{{YYYY}}-{{MM}}-{{DD}}T{{hh}}:{{mm}}:{{ss}}.{{millisecond}}{{nanosecond}}'
    utcToZone = options.utcToZone; // e.g. 'UTC+09:00:00' or '[Asia/Tokyo]'
    zoneToUtc = options.zoneToUtc; // e.g. 'UTC+09:00:00' or '[Asia/Tokyo]'
    // ...
    return view;
};



/*
 * CivilDate
 */
// equivalent CivilDate constructor
var date = '2017-11-15'; // conform to ISO standard

// regexp validator - conform to ISO standard
(/^\d\d\d\d-\d\d-\d\d$/).test(date); // true

// equivalent CivilDate properties
var year = Number(date.split(/\D/)[0]); // 2017
var month = Number(date.split(/\D/)[1]); // 11
var day = Number(date.split(/\D/)[2]); // 15
console.log([year, month, day]); // [2017, 11, 15]

// equivalent CivilDate.prototype.plus
var civilDate2 = Date.isoStringDatePlus('2017-11-15', {months: 1}); // '2017-12-15'
var civilDate2 = Date.isoStringDatePlus('22:39:11.91499', {months: 1}); // throw error - invalid date
var civilDate2 = Date.isoStringDatePlus('2017-11-15T22:39:11.91499', {months: 1}); // '2017-12-15T22:39:11.91499'

// equivalent CivilDate.prototype.withTime
var civilDateTime = date + 'T' + '22:39:11.91499'; // '2017-11-15T22:39:11.91499'



/*
 * CivilTime
 */
// equivalent CivilTime constructor
var time = '22:39:11.91499'; // conform to ISO standard

// regexp validator - conform to ISO standard
(/^\d\d:\d\d:\d\d(\.\d+){0,1}$/).test(time); // true

// equivalent CivilTime properties
var hour = Number(time.split(/\D/)[0]); // 22
var minute = Number(time.split(/\D/)[1]); // 39
var second = Number(time.split(/\D/)[2]); // 11
var millisecond = Number(
    ((time.split(/\D/)[3] || '') + '000000000').slice(0, 3)
); // 914
var nanosecond = Number(
    ((time.split(/\D/)[3] || '') + '000000000').slice(3, 9)
); // 990000
console.log([hour, minute, second, millisecond, nanosecond]); // [22, 39, 11, 914, 990000]

// equivalent CivilTime.prototype.plus
var civilTime2 = Date.isoStringTimePlus('2017-11-15', {hours: 2, minutes: 4}); // throw error - invalid time
var civilTime2 = Date.isoStringTimePlus('22:39:11.91499', {hours: 2, minutes: 4}); // '24:43:11.91499'
var civilTime2 = Date.isoStringTimePlus('2017-11-15T22:39:11.91499', {hours: 2, minutes: 4}); // '2017-11-15T24:43:11.91499'

// equivalent civilTime.prototype.withDate
var civilDateTime = '2017-11-15' + 'T' + time; // '2017-11-15T22:39:11.91499'



/*
 * CivilDateTime
 */
// equivalent CivilDateTime constructor
var datetime = '2017-11-15T22:39:11.91499'; // conform to ISO standard

// regexp validator - conform to ISO standard
(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+){0,1}$/).test(datetime); // true

// equivalent CivilDateTime properties
var year = Number(datetime.split(/\D/)[0]); // 2017
var month = Number(datetime.split(/\D/)[1]); // 11
var day = Number(datetime.split(/\D/)[2]); // 15
var hour = Number(datetime.split(/\D/)[3]); // 22
var minute = Number(datetime.split(/\D/)[4]); // 39
var second = Number(datetime.split(/\D/)[5]); // 11
var millisecond = Number(
    ((datetime.split(/\D/)[6] || '') + '000000000').slice(0, 3)
); // 914
var nanosecond = Number(
    ((datetime.split(/\D/)[6] || '') + '000000000').slice(3, 9)
); // 990000
console.log([year, month, day, hour, minute, second, millisecond, nanosecond]); // [2017, 11, 15, 22, 39, 11, 914, 990000]

// equivalent CivilDateTime.prototype.from
var civilDateTime = '2017-11-15' + 'T' + '22:39:11.91499'; // datetime

// equivalent CivilDateTime.prototype.plus
var civilDateTime2 = Date.isoStringDateTimePlus(datetime, {days: 3, hours: 4, minutes: 2, seconds: 12}); // '2017-11-19T02:41:23.91499'

// equivalent CivilDateTime.prototype.toCivilDate
var civilDate = datetime.slice(0, 10); // '2017-11-15'

// equivalent CivilDateTime.prototype.toCivilTime
var civilTime = datetime.slice(11); // '22:39:11.91499'

// equivalent CivilDateTime.prototype.withZone
var zonedInstant = { datetime: datetime, zone: '[Asia/Tokyo]' }; // {datetime: "2017-11-15T22:39:11.91499", zone: "[Asia/Tokyo]"}



/*
 * Instant
 */
// equivalent Instant constructor
var instant = '2017-11-15T22:39:11.91499'; // conform to ISO standard

// regexp validator - conform to ISO standard
(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+){0,1}$/).test(instant); // true

// equivalent Instant properties
var milliseconds = Date.isoStringInstantGetMilliseconds(instant); // 1510785551914
var nanoseconds = Number(
    ((instant.split(/\D/)[6] || '') + '000000000').slice(3, 9)
); // 990000
console.log([milliseconds, nanoseconds]); // [1510785551914, 990000]

// equivalent CivilDateTime.prototype.withZone
var zonedInstant = { datetime: instant, zone: '[Asia/Tokyo]' }; // {datetime: "2017-11-15T22:39:11.91499", zone: "[Asia/Tokyo]"}



/*
 * ZonedInstant
 */
// equivalent ZonedInstant constructor
var zonedInstant = { datetime: '2017-11-16T07:39:11.91499', zone: '[Asia/Tokyo]' };

// regexp validator - conform to ISO standard
(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+){0,1}$/).test(zonedInstant.datetime); // true

// equivalent ZonedInstant properties
var milliseconds = Number(Date.isoStringZonedInstantView(zonedInstant.datetime, {
    template: '{{millisecond}}',
    utcToZone: zonedInstant.zone
})); // 1510817951914
var nanoseconds = Number(Date.isoStringZonedInstantView(zonedInstant.datetime, {
    template: '{{nanosecond}}',
    utcToZone: zonedInstant.zone
})); // 990000
console.log([milliseconds, nanoseconds]); // [1510785551914, 990000]

// equivalent ZonedInstant.prototype.toCivilDateTime
var civilDateTime = Date.isoStringZonedInstantView(zonedInstant.datetime, {
    template: '{{YYYY}}-{{MM}}-{{DD}}T{{hh}}:{{mm}}:{{ss}}.{{millisecond}}{{nanosecond}}',
    zoneToUtc: zonedInstant.zone
}); // '2017-11-15T22:39:11.914990000'

// equivalent ZonedInstant.prototype.toCivilDate
var civilDateTime = Date.isoStringZonedInstantView(zonedInstant.datetime, {
    template: '{{YYYY}}-{{MM}}-{{DD}}',
    zoneToUtc: zonedInstant.zone
}); // '2017-11-15'

// equivalent ZonedInstant.prototype.toCivilTime
var civilDateTime = Date.isoStringZonedInstantView(zonedInstant.datetime, {
    template: '{{hh}}:{{mm}}:{{ss}}.{{millisecond}}{{nanosecond}}',
    zoneToUtc: zonedInstant.zone
}); // '22:39:11.914990000'

// equivalent ZonedInstant.prototype.toInstant
var civilDateTime = Date.isoStringZonedInstantView(zonedInstant.datetime, {
    template: '{{YYYY}}-{{MM}}-{{DD}}T{{hh}}:{{mm}}:{{ss}}.{{millisecond}}{{nanosecond}}',
    zoneToUtc: zonedInstant.zone
}); // '2017-11-15T22:39:11.914990000'



/*
 * with function (all civil objects)
 */
var myCivilDate = '2016-02-29'; // conform to ISO standard
var newCivilDate = myCivilDate.replace((/(\d\d\d\d)-(\d\d)-(\d\d)/), '2017-03-$3'); // '2017-03-29



/*
 * plus function (all objects)
 */
var myCivilDate = '2016-02-29'; // conform to ISO standard
var newCivilDate = Date.isoStringDatePlus(myCivilDate, {year: 2017, month: 2}); // '2017-04-28'
@timrwood
Copy link
Contributor

Here are some cons to add to your list.

  • Each method needs to parse a string and output a string.

Parsing and formatting are the most expensive operations in moment.js. I don't think requiring every operation to incur these costs is efficient.

  • The ISO standard is more flexible than YYYY-MM-DDThh:mm:ss, making regex based manipulation difficult to handle correctly.

All of the following are valid ISO 8601 dates.

2012-01-02
20120102
+20012-01-02
120102

All of the following are valid ISO 8601 date times.

2012-01-02T03:04
2012-01-02 0304
2012-01-02 030405.123
2012-01-02T24:00

Most of your example code would not work correctly with these valid ISO 8601 strings.

@kaizhu256
Copy link
Contributor Author

kaizhu256 commented Nov 16, 2017

@timrwood

  1. there is no intention to support general ISO 8601. that is out-of-scope imo. if input fails the provided regexp validators, then they are invalid inputs. users are already familiar with the specific iso format YYYY-MM-DDThh:mm:ss.123456789, so enforcing only it and its date and time subsets, shouldn't create any confusion.

  2. not supporting general ISO 8601 alleviates some of the performance parsing/formatting issues. the string YYYY-MM-DDThh:mm:ss.123456789 is well-defined and bounded, making it ideal for engine optimizations. the slowest function is probably Date.isoStringZonedInstantView, but i consider this function to be used mainly client-side at presentation-level, so it shouldn't hurt server-side operations.

@hollowdoor
Copy link

Prior art.

https://docs.python.org/2/library/datetime.html
https://docs.oracle.com/javase/tutorial/datetime/iso/date.html

I think the confusion comes from thinking of clock time as a subset of calendar time which actually isn't the case. Clock time, and calendar time are separate unit systems that only happen to be appended to one, or the other because they measure the same thing.

Apparently not everyone feels that getting the date alone is easy even when there is a method.

https://stackoverflow.com/search?q=get+date+only

There's no way to seek for questions about confusion with constructor names in stackoverflow because the topic is relatively subjective.

@dilijev
Copy link

dilijev commented Nov 21, 2017

@timrwood

Each method needs to parse a string and output a string.

I tend to agree, but devil's advocate:

Since this will be built-in to the engines, the semantics may look like that, but what engines might actually do (from JS side) is create a pseudo-string object which is passed around, but on which properties that are less expensive to manipulate can be manipulated. Only when creating this object initially and when a string is actually needed will parse/format need to be done.

@kaizhu256
Copy link
Contributor Author

kaizhu256 commented Nov 21, 2017

here's test-code and benchmark using Date.isoStringInstantGetMilliseconds to parse a string and output a string, on my 1.2 GHz macbook (~1 million ops / second). and as @dilijev pointed out, engines can internally represent the string as a struct with a toString() view.

result:
463 ms to do 1000000 Date.isoStringInstantGetMilliseconds operations parsing string
1193 ms to do 1000000 Date.isoStringInstantGetMilliseconds operations parsing string and converting back to string

/*jslint bitwise: true, node: true*/
'use strict';
var ii, resultList, timeElapsed;
Date.isoStringInstantGetMilliseconds = function (isoDateTime) {
/*
 * this function will return the
 * integer value representing the number of milliseconds elapsed
 * from 1970-01-01 00:00:00.000 UTC, without regarding leap seconds
 */
    var milliseconds;
    milliseconds = new Date(isoDateTime);
    milliseconds = milliseconds.getTime() - 60 * 1000 * milliseconds.getTimezoneOffset();
    return milliseconds;
};
resultList = new Array(0x10);

timeElapsed = Date.now();
for (ii = 0; ii < 1000000; ii += 1) {
    // parse a string
    resultList[ii & 0xf] = Date.isoStringInstantGetMilliseconds('2017-11-21T00:00:00.' + (ii & 0xff));
}
timeElapsed = Date.now() - timeElapsed;
console.error('resultList = ' + JSON.stringify(resultList, null, 4));
console.error(timeElapsed + ' ms to do ' + ii +
    ' Date.isoStringInstantGetMilliseconds operations parsing string');

timeElapsed = Date.now();
for (ii = 0; ii < 1000000; ii += 1) {
    // parse a string and output a string
    resultList[ii & 0xf] = new Date(
        Date.isoStringInstantGetMilliseconds('2017-11-21T00:00:00.' + (ii & 0xff))
    ).toISOString();
}
timeElapsed = Date.now() - timeElapsed;
console.error('resultList = ' + JSON.stringify(resultList, null, 4));
console.error(timeElapsed + ' ms to do ' + ii +
    ' Date.isoStringInstantGetMilliseconds operations parsing string and converting back to string');



/*
output:

resultList = [
    1511222400480,
    1511222400490,
    1511222400500,
    1511222400510,
    1511222400520,
    1511222400530,
    1511222400540,
    1511222400550,
    1511222400560,
    1511222400570,
    1511222400580,
    1511222400590,
    1511222400600,
    1511222400610,
    1511222400620,
    1511222400630
]
469 ms to do 1000000 Date.isoStringInstantGetMilliseconds operations parsing string
resultList = [
    "2017-11-21T00:00:00.480Z",
    "2017-11-21T00:00:00.490Z",
    "2017-11-21T00:00:00.500Z",
    "2017-11-21T00:00:00.510Z",
    "2017-11-21T00:00:00.520Z",
    "2017-11-21T00:00:00.530Z",
    "2017-11-21T00:00:00.540Z",
    "2017-11-21T00:00:00.550Z",
    "2017-11-21T00:00:00.560Z",
    "2017-11-21T00:00:00.570Z",
    "2017-11-21T00:00:00.580Z",
    "2017-11-21T00:00:00.590Z",
    "2017-11-21T00:00:00.600Z",
    "2017-11-21T00:00:00.610Z",
    "2017-11-21T00:00:00.620Z",
    "2017-11-21T00:00:00.630Z"
]
1130 ms to do 1000000 Date.isoStringInstantGetMilliseconds operations parsing string and converting back to string
*/

screen shot 2017-11-21 at 11 58 54 am

@ljharb
Copy link
Member

ljharb commented Nov 21, 2017

Performance benchmarks are interesting, but a million operations taking 0.4 vs 1.2 ms, when 99% of use cases will be doing < 10 operations in a given second, isn't really significant.

What's far more important than microbenchmarks is clarity, usability, and what's idiomatic for JS.

@kaizhu256
Copy link
Contributor Author

kaizhu256 commented Nov 25, 2017

idiomatic for whom?

people who primarily write javascript?
or people who primarily write c#/c++/java?

javascript should adopt solutions best-suited for javascript use-cases, which means datatypes (string in this case) that are more JSON-friendly and baton-passable across systems that recognize JSON. this is a unique criteria not placed highly in c#/c++/java, and why their idiomatic solutions are not always ideal for js.

@ljharb
Copy link
Member

ljharb commented Nov 25, 2017

"Idiomatic" is indeed subjective - but I primarily write JavaScript (and have never written C#, and very little C++ or Java), and I find the current proposal far, far more idiomatic than your suggestion of stringly typed static methods. As far as JSON-friendly, any date format you choose in your app can be used for that.

@kaizhu256
Copy link
Contributor Author

kaizhu256 commented Nov 25, 2017

i proposed extending Date with 5 static functions to cover all the constructors and methods for CivilDate, CivilDateTime, CivilTime, Instant, and ZonedInstant. how is that large quantity?

@ljharb
Copy link
Member

ljharb commented Nov 25, 2017

Fair; I'll edit that part out.

@pipobscure
Copy link
Collaborator

I tend to think that Date should be deprecated and definitely not mixed beyond routes to interoperation.

@kaizhu256
Copy link
Contributor Author

I tend to think that Date should be deprecated and definitely not mixed beyond routes to interoperation.

and replace it with what? CivilDate, CivilDateTime, CivilTime, Instant, and ZonedInstant? do you honestly think its easier to debug-and-fix code that has to context-switch between 5 confusing temporal-types, instead of code that just has to deal with a single Date-type (and its serialized canonical-ISOString/JSON representation)? because its not. and you will be hard-pressed to come up with a tc39-proposal that will be easier-to-use/JSON-serialize than existing Date.

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

No branches or pull requests

7 participants