Parsing ISO 8601 like duration in Luxon - javascript

I have duration string that looks like:
1:16.352
where 1 is minutes part, 16 is seconds part and 352 is millisecond part.
I wanted to use Duration.fromISOTime but I get:
{
"reason": "unparsable",
"explanation": "the input \"1:16.352\" can't be parsed as ISO 8601"
}
Is there a clean way of parsing such duration in Luxon?

Duration.fromISOTime does not work since 1:16.352 is not an ISO 8601 time string, the hour part is missing (see ISO 8601 Times).
A workaround to build a Luxon Duration object could be the following:
const DateTime = luxon.DateTime;
const Duration = luxon.Duration;
const startOfHour = DateTime.local().startOf('hour').toMillis();
const dt = DateTime.fromFormat("1:16.352", "m:ss.SSS"). toMillis();
const dur = Duration.fromMillis(dt - startOfHour);
console.log(dur.toFormat("m 'minute' s 'second' S 'millis'"));
<script src="https://cdn.jsdelivr.net/npm/luxon#1.26.0/build/global/luxon.js"></script>

Similarly to #VincenzoC I adjusted my input string:
const Duration = luxon.Duration;
var output;
const durationInput = "1:16.352"
if (durationInput.match(/:/g) || [].length === 1) {
const semicolonLocation = durationInput.indexOf(":");
if (semicolonLocation === 1) {
output = "00:0" + durationInput;
}
if (semicolonLocation === 2) {
output = "00:" + durationInput;
}
}
console.log(Duration.fromISOTime(output));
<script src="https://cdn.jsdelivr.net/npm/luxon#1.26.0/build/global/luxon.js"></script>

As I mentioned in the comments, you can use the fromObject static method combined with simply splitting the input into minutes, seconds, and milliseconds configuration options. In your case a trivial regular expression (\d+):(\d+)\.(\d+) should do the trick, no temporary dates or normalization required.
const { Duration } = luxon;
const durationInput = "1:16.352";
const fromCustom = (input) => {
const [, minutes, seconds, milliseconds ] = input.match(/(\d+):(\d+)\.(\d+)/);
return Duration.fromObject({
minutes, seconds, milliseconds
});
};
console.log(fromCustom(durationInput));
<script src="https://cdn.jsdelivr.net/npm/luxon#1.26.0/build/global/luxon.js"></script>

Related

Converting unix timestamp to Milliseconds in luxon.js

I have string data in the format "hh:mm" e.g. 05:00. I want it in Milliseconds e.g 1800000
console.log(DateTime.fromISO("05:00") and i get the following output: 1654574400000 but what i want it in seconds so i can compare it against a different value. I have tried putting .toMillis() at the end
console.log(DateTime("05:00")).toMillis();
and it comes back with "Unhandled Rejection (TypeError): Class constructor DateTime cannot be invoked without 'new'".
You can parse "05:00" as a Duration, using Duration.fromISOTime that:
Create a Duration from an ISO 8601 time string.
and then display its value using as(unit):
Return the length of the duration in the specified unit.
Example:
const Duration = luxon.Duration;
console.log(Duration.fromISOTime('05:00').as('milliseconds'));
<script src="https://cdn.jsdelivr.net/npm/luxon#2.4.0/build/global/luxon.min.js"></script>
When a time is passed to fromISO, the current date is used. To get the time in milliseconds, parse it to a DateTime and subtract a DateTime for the start of the day, e.g.
let DateTime = luxon.DateTime;
function timeToMs(time) {
let then = DateTime.fromISO(time);
let now = DateTime.fromISO("00:00");
return then - now;
}
console.log(timeToMs('05:00'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/2.4.0/luxon.min.js"></script>
You can also use plain JS:
function timeToMS(time) {
let [h, m, s, ms] = time.split(/\D/);
return h*3.6e6 + (m||0)*6e4 + (s||0)*1e3 + (ms||0)*1;
}
console.log(timeToMS('05:00'));
console.log(timeToMS('01:01:01.001'));

How to return epoch of all midnights in given time span?

I have a time span between two epoch times (in seconds). I want a function that returns the epoch times (in seconds) of all midnights within that time span.
In pseudocode I would want something like this:
const epoch_start = 1600000;
const epoch_end = 16040000;
function getMidnights(start, end){
// do your magic
}
console.log(getMidnights(epoch_start, epoch_end));
I would expect the return of that function to look like this: [1600020, 1600400] (these are just example values).
What would be the most efficient way to do this?
My ideas were: get unique list of days within range and return their midnight.
You might step by day rounding by day:
const epoch_start = 1600000000;
const epoch_end = 1600400000;
const day = 86400;
function getMidnights(start, end){
midnites = [];
while (start < end) {
midnites.push((start/day + .5|0)*day);
start += day;
}
return midnites;
}
console.log(getMidnights(epoch_start, epoch_end))
console.log('* check it *')
console.log(getMidnights(epoch_start, epoch_end).map(t => new Date(t*1e3)));

Formatting Duration in Correct Format `HH:MM`

I have some logic designed to generate a formatted duration based on an inputted start and stop time:
getFormattedDuration = async (stopValue, startValue) => {
const durValue = moment(stopValue, "H:mm").diff(moment(startValue, "H:mm"));
const t = moment.duration(durValue);
const duration = {
hours: t.hours(),
minutes: t.minutes()
}
const formattedDuration = `0${duration.hours}:${duration.minutes}`;
return formattedDuration;
// I'm prepending a `0` here because I know the duration will never be above a certain limit - i.e. it will never get to a double digit number for hours.
}
This works for the most part. But on occasion I will end up with something like this for output:
duration: Object {
"hours": 0,
"minutes": 8,
}
formattedDuration: 00:8
What I want here is 00:08. How can I pad with zeroes, as necessary, to ensure I get a correctly formatted duration value - which should be in this format hh:mm. Does moment.js have a function I can use to handle this?
You can use the format() function from moment.js.
const m = moment('00:8','HH:mm').format('HH:mm');
console.log(m) // "00:08"
m = moment('00:8','HH:mm').format('HH:mm');
console.log(m)
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
You could use to .padStart() to ensure there are leading zeros.

How to calculate the difference between 2 dates

All I am trying to do is this.
Let's say in Google Sheets, I have a page which has the following columns:
In column I3 I have the date of 11/30/19.
In column J3, I have Today's date of 4/30/20.
I want to be able to calculate the difference between these two dates and tell me how many months has it been since 11/30/19.
Currently, I think the code is working somewhat but the result I get is:
[20-05-02 01:43:18:650 MDT] 5 months, 6 days
[20-05-02 01:43:18:656 MDT] 5 months, 6 days
[20-05-02 01:43:18:660 MDT] 5 months, 6 days
But the date calculations are still wrong. For example from Jan 1st - Jan 25, 2020, it shows 5 month and 6 days.
Also the loop I have inside Filter 1, is just calculating the first available date and then it does this three times instead of going to the next record and calculate.
I have the following code so far:
function myFunction() {
}
var moment = Moment.load();
/**
* #summary gets date difference
* #param {Date} startDate
* #param {Date} endDate
* #returns {string}
*/
function getDuration(startDate, endDate) {
const start = moment(startDate);
const end = moment(endDate);
const units = ['years', 'months', 'days'];
const lastIndex = units.length - 1;
const parts = units
.map((unit,i) => {
const diff = Math.floor(end.diff(start, unit, true));
if (diff > 0 || i === lastIndex) {
end.subtract(unit, diff);
return `${diff} ${unit}`;
}
})
.filter(Boolean);
return parts.join(', ');
}
function Filter2() { // Calculate the Time
const spread = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spread.getSheets();
const [ sheet_1, sheet_2 ] = sheets;
const row = sheet_1.getRange("A:M");
const arr_col = sheet_1.getRange("I3:I50");
const lastSeen_col = sheet_1.getRange("J3:J50");
const startDate = arr_col.getValue();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var headerRowNumber = 2;
var rows = sheet.getDataRange().offset(headerRowNumber, 0, sheet.getLastRow() - headerRowNumber).getValues();
var filter = rows.filter(row => row[9] !== '');
// var digest = filter[0][9];
for(var i=0;i<filter.length; i++){
var res = getDuration(startDate, lastSeen_col);
Logger.log(res);
}
}
Why Dec, 31 1969
31st of December, 1969 in GMT-7 timezone offset is January 1st, 1970 00:00:00 UTC, which is the unix epoch. Therefore it is likely to be caused by an invalid date format passed to the Moment class instance.
Problem
getRange() method call returns an instance of Range from which you have to extract value via getValue() / getValues() before being able to use it. With that in mind, let's track what's going on in your script:
var ArrRow = sheet_1.getRange("I3:I") returns an instance of Range
mydata1 is defined somewhere globally (let's assume it holds an instance of Date)
getDuration is thus called like this: getDuration( <Range>, <Date> )
startDate and endDate are respectively an instance of Range and Date
start and end hold results of invoking the moment factory that creates a wrapper around Date object.
Step 5 is most likely to be the culprit, as per Moment library docs, moment() can accept String, Number, Date instances as well as format options, but start recieves a Range instance instead.
Possible solution
First, you need to ensure you pass in correct types (this why a lot of folks prefer TypeScript - if that's not your cup of tea, at least start using JSDoc - it will save you a ton of debug time).
I don't know how big the I3:I range is, so I assumed from the context that it is a single cell containing start date. Also note that I removed the var sld = new Date(dateString) assignment since you return a human readable string from getDuration() of format Y years, M months, D days which is not a dateString that Date constructor can accept.
Additionally, I would suggest changing forEach() to map() method for cleaner and less side effect prone code. General rule of thumb is that if input and output are of the same type, you likely want to use a mapping function.
function myFunction() {
Filter2();
}
function Filter2() {
const spread = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spread.getSheets();
const [ sheet_1, sheet_2 ] = sheets;
const arr_col = sheet_1.getRange("I3:I");
const startDate = arr_col.getValue();
var dateString = getDuration(startDate, mydata1);
Logger.log(dateString);
}
var moment = Moment.load();
/**
* #summary gets date difference
* #param {Date} startDate
* #param {Date} endDate
* #returns {string}
*/
function getDuration(startDate, endDate) {
const start = moment(startDate);
const end = moment(endDate);
const units = ['years', 'months', 'days'];
const lastIndex = units.length - 1;
const parts = units
.map((unit,i) => {
const diff = Math.floor(end.diff(start, unit, true));
if (diff > 5 || i === lastIndex) {
end.subtract(unit, diff);
return `${diff} ${unit}`;
}
})
.filter(Boolean);
return parts.join(', ');
}
Notes
The answer assumes you use V8 engine (if you don't - switch to it, old one will be deprecated some time in the future, and as a bonus you get to use all the juciest language features [well, most of them]).
References
Date constructor docs on MDN
Range class docs
getValue() method docs
getValues() method docs

Get utc offset from timezone in Javascript

I need a Javascript function that given a timezone, returns the current UTC offset.
For example, theFuncIneed('US/Eastern') -> 240
In general, this is not possible.
US/Eastern is an identifier for a time zone. (It's actually an alias to America/New_York, which is the real identifier.)
240 is a time zone offset. It's more commonly written as -04:00 (Invert the sign, divide by 60).
The US Eastern Time Zone is comprised of both Eastern Standard Time, which has the offset of -05:00 and Eastern Daylight Time, which has the offset of -04:00.
So it is not at all accurate to say US/Eastern = 240. Please read the timezone tag wiki, especially the section titled "Time Zone != Offset".
Now you did ask for the current offset, which is possible. If you supply a date+time reference, then you can resolve this.
For the local time zone of the computer where the javascript code is executing, this is built in with .getTimeZoneOffset() from any instance of a Date object.
But if you want it for a specific time zone, then you will need to use one of the libraries I listed here.
It has become possible nowaday with Intl API:
The implementation of Intl is based on icu4c. If you dig the source code, you'll find that timezone name differs per locale, for example:
for (const locale of ["ja", "en", "fr"]) {
const timeZoneName = Intl.DateTimeFormat(locale, {
timeZoneName: "short",
timeZone: "Asia/Tokyo",
})
.formatToParts()
.find((i) => i.type === "timeZoneName").value;
console.log(timeZoneName);
}
Fortunately, there is a locale, Interlingua (the langauge tag is ia), which uses the same pattern (ex. GMT+11:00) for timezone names.
The snippet below can meed your need:
const getOffset = (timeZone) => {
const timeZoneName = Intl.DateTimeFormat("ia", {
timeZoneName: "short",
timeZone,
})
.formatToParts()
.find((i) => i.type === "timeZoneName").value;
const offset = timeZoneName.slice(3);
if (!offset) return 0;
const matchData = offset.match(/([+-])(\d+)(?::(\d+))?/);
if (!matchData) throw `cannot parse timezone name: ${timeZoneName}`;
const [, sign, hour, minute] = matchData;
let result = parseInt(hour) * 60;
if (sign === "+") result *= -1;
if (minute) result += parseInt(minute);
return result;
};
console.log(getOffset("US/Eastern")); // 240
console.log(getOffset("Atlantic/Reykjavik")); // 0
console.log(getOffset("Asia/Tokyo")); // -540
This way can be a little tricky but it works well in my production project. I hope it helps you too :)
Update
Many thanks to Bort for pointing out the typo. I have corrected the snippet.
Following function can be used to return the UTC offset given a timezone:
const getTimezoneOffset = (timeZone, date = new Date()) => {
const tz = date.toLocaleString("en", {timeZone, timeStyle: "long"}).split(" ").slice(-1)[0];
const dateString = date.toString();
const offset = Date.parse(`${dateString} UTC`) - Date.parse(`${dateString} ${tz}`);
// return UTC offset in millis
return offset;
}
It can be used like:
const offset = getTimezoneOffset("Europe/London");
console.log(offset);
// expected output => 3600000
You can do this using moment.js
moment.tz('timezone name').utcOffset()
Although this involves using moment-timezone.js
The answer of #ranjan_purbey results in NaN for me and the answer of #Weihang Jian throws an exception in Chrome (but works in Firefox).
Therefore, based on all the answers I came up with the following function which is basically a combination of both answers working together successfully for me:
function getTimeZoneOffset(timeZone) {
const date = new Date().toLocaleString('en', {timeZone, timeZoneName: 'short'}).split(' ');
const timeZoneName = date[date.length - 1];
const offset = timeZoneName.slice(3);
if (!offset) {
return 0;
}
const matchData = offset.match(/([+-])(\d+)(?::(\d+))?/);
if (!matchData) {
throw new Error(`Cannot parse timezone name: ${timeZoneName}`);
}
const [, sign, hour, minute] = matchData;
let result = parseInt(hour, 10) * 60;
if (sign === '+') {
result *= -1;
}
if (minute) {
result += parseInt(minute, 10);
}
return result;
}

Categories

Resources