Note that bellow I included also the day for extensibility purposes. If not needed at all then can be easily removed.
Solution 1 - string sorting key YYYY-MM-DD-LLLLL
The following solution creates a key for sorting without using the Date
object and considers also that a string without month or day should be treated first in an ascending sorting order.
The sorting key is: YYYY-MM-DD-LLLLL
where L
is length.
const ensureDigits = (value, digits) => {
return (Math.pow(10, digits) + value).toString().slice(- digits);
};
const sortDateStrings = (dateStrLst, descending) => {
return dateStrLst
.map(dateStr => {
const parts = dateStr.value.split('/');
const year = parts[0];
const month = ensureDigits(parts[1] || 1, 2);
const day = ensureDigits(parts[2] || 1, 2);
const length = ensureDigits(dateStr.value.length, 5);
return { item: dateStr, sortKey: `${year}-${month}-${day}-${length}` }
})
.sort((model1, model2) =>
model1.sortKey.localeCompare(model2.sortKey) * (descending ? -1 : 1))
.map(model => model.item);
}
const dateStrLst1 = [{value: "2015"}, {value:"2015/3"}, {value: "2015/10"}];
const dateStrLst2 = [{value: "1932"}, {value:"1933/11"}, {value: "1932/1"}];
console.log(sortDateStrings(dateStrLst1).map(item => item.value));
console.log(sortDateStrings(dateStrLst2).map(item => item.value));
console.log(sortDateStrings(dateStrLst1, true).map(item => item.value));
console.log(sortDateStrings(dateStrLst2, true).map(item => item.value));
Solution 2 - order by date, then by length, using Lodash
const sortDateStrings = (dateStrLst, descending) => {
const model = dateStrLst
.map(dateStr => {
const parts = dateStr.value.split('/');
const year = parts[0];
const month = (parts[1] - 1) || 0;
const day = parts[2] || 1;
return { item: dateStr, date: new Date(year, month, day), length: dateStr.value.length };
});
const direction = descending ? 'desc' : 'asc';
return _.orderBy(model, ["date", "length"], [direction, direction])
.map(modelItem => modelItem.item);
};
const dateStrLst1 = [{value: "2015"}, {value:"2015/3"}, {value: "2015/10"}];
const dateStrLst2 = [{value: "1932"}, {value:"1933/11"}, {value: "1932/1"}];
console.log(sortDateStrings(dateStrLst1).map(item => item.value));
console.log(sortDateStrings(dateStrLst2).map(item => item.value));
console.log(sortDateStrings(dateStrLst1, true).map(item => item.value));
console.log(sortDateStrings(dateStrLst2, true).map(item => item.value));
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.2/lodash.js"></script>
</head>
<body>
</body>
</html>
WARNING: parsing date
// valid ISO 8601 string
const date1 = new Date(Date.parse('1932'));
Fri Jan 01 1932 02:00:00 GMT+0200 (GTB Standard Time)
// not a valid string
const date2 = new Date(Date.parse('1932/1'));
Fri Jan 01 1932 00:00:00 GMT+0200 (GTB Standard Time)
// not a valid string
const date3 = new Date(Date.parse('1932/01'));
Fri Jan 01 1932 00:00:00 GMT+0200 (GTB Standard Time)
date1.getTime() == date2.getTime();
false
also the same happens with:
// not a valid ISO 8601 string
const date2 = new Date(Date.parse('1932-1'));
Fri Jan 01 1932 00:00:00 GMT+0200 (GTB Standard Time)
but this one is correct:
// valid ISO 8601 string
const date3 = new Date(Date.parse('1932-01'));
Fri Jan 01 1932 02:00:00 GMT+0200 (GTB Standard Time)
and the same problem happens also with:
// incorrect usage of constructor
new Date(1932);
Thu Jan 01 1970 02:00:01 GMT+0200 (GTB Standard Time)
// correct usage of constructor
new Date(1932, 0);
Fri Jan 01 1932 00:00:00 GMT+0200 (GTB Standard Time)
But this last one new Date(1932)
is incorrect as the constructor allows at least year + month as in new Date(year, month[, date[, hours[, minutes[, seconds[, milliseconds]]]]]);
.
Note that before I used new Date(Date.parse(...))
because:
Note: parsing of date strings with the Date constructor (and Date.parse, they are equivalent) is strongly discouraged due to browser differences and inconsistencies.
Date
can parse:
A string representing an RFC2822 or ISO 8601 date (other formats may be used, but results may be unexpected).
In respect to this question, ISO 8601 formats are (notice that days and months are two digits only):
Year:
YYYY (eg 1997)
Year and month:
YYYY-MM (eg 1997-07)
Complete date:
YYYY-MM-DD (eg 1997-07-16)
In respect to this question, RFC2822 formats are:
date-time = [ day-of-week "," ] date FWS time [CFWS]
date = day month year
Therefore I find no format to match something similar to 1932/1
in order to parse consistently across browsers.
Performance wise might be to stick with ISO 8601
format as is usually the first one being checked, then the RFC2822
.
If you are using momentjs ensure that the format you are about to parse is supported by checking parsing strings section.
Conclusion:
Create date objects consistently in order to obtain correct results:
- Date constructor: (gives local date) -
new Date(1932, 0)
- Date.parse() (gives UTC date when used with ISO 8601 date only) -
Date.parse('1932')
same as Date.parse('1932-01')
- notice the two digits for month
Date.parse(<ISO_8601_string>)
gives UTC date (when date only) because:
Support for ISO 8601 formats differs in that date-only strings (e.g. "1970-01-01") are treated as UTC, not local.