Extend TMS WEB Core with JS Libraries with Andrew: Luxon

TMS Software Delphi  Components
The last couple of posts covered FlatPickr, a popular and very capable datepicker JS Library.  We looked at many of its options, and looked at two completely different ways to add it to our TMS WEB Core projects.   What we didn’t really delve too much into is the whole area of date and time formats, timezones, internationalization, and the differences between date-handling in Delphi and JavaScript. Well, we did a little, but just enough to get through some examples.  This time out, we’ll go into a bit more detail and also introduce the latest JS Library to our repertoire – Luxon – which describes itself as “a powerful, modern, and friendly wrapper for JavaScript dates and times.”  I’m not at all sure that I buy the “friendly” bit, but it is indeed powerful and modern, so let’s see where it fits in.

Motivation.

Long-time Delphi developers are likely to be pretty familiar with TDateTime and its various advantages and shortcomings.  And even if you’re relatively new to Delphi and haven’t had much of an opportunity to interact with TDateTime, there’s not much there to trip you up and, by and large, it isn’t likely to garner much attention on its own. This is a good thing, of course. When dealing with JavaScript, however, the nice and comfy TDateTime is replaced with a rather sinister JavaScript date format that is none of these things.  Sure, it is easy enough to get started with a JavaScript date, but it really is an entirely different beast.  Moving between the two can be a challenge, and even just trying to do the tiniest bit of formatting can sometimes be a lot more trouble than might seem possible.  So in this post we’re going to go over a bunch of this kind of stuff.  Get it all out in one fell swoop, rip off the band-aid so to speak, so we can candidly and confidently move on to other topics, but have this in our back pocket when we need it.  And believe me, we’ll be needing it!

Epic Epoch.

Let’s dip our toe into the shallow end first and quickly go over what TDateTime is. Delphi uses the TDateTime class to encode, naturally, a date and a time.  It does this in a very simple way – by using a floating point number (specifically, a double) where the whole part of the number represents the number of days since 1899-12-30 and the fractional part of the number represents the time as a fraction of a day.  Noon would be represented as .5, 18:00 would be represented as .75, and so on.  The choice of 1899-12-30 is somewhat arbitrary, but I ran across this link which described it this way:
It appears that the reason for Delphi starting at 30 Dec 1899 is to make it as compatible as possible with Excel while at the same time not adopting Excel’s incorrectness about dates. Historically, Excel played second fiddle to Lotus 1-2-3. Lotus (which may have got this error from Visicalc) incorrectly considered 1900 to be a leap year hence a value of 60 gives you 29 Feb 1900 in Excel but is interpreted as 28 Feb 1900 in Delphi due to Delphi starting 1 day before. From the 01 Mar 1901 the two date systems give the same result for a given number. 

Great, another reason to loathe Excel. Like there weren’t enough already! The actual details around this aren’t going to matter all that much, unless of course you happen to work with a lot of dates around that time (or earlier) where this might be an issue.  And we don’t have to worry much about the other direction either.  As a double, the maximum date that can be represented with  TDateTime is at least 9999-12-31.  It can actually represent dates after that, but things get a little squirrelly when the year is more than four digits.

Unix (and Linux) store this information as integers, the number of seconds since 1970-01-01 00:00:00.  Using signed 32-bit integers, this means that the maximum value that can be stored will overflow in 2038.  Curiously, the minimum date is in 1901.  Fortunately, it seems that 64-bit systems wisely updated these to use 64-bit integers, so this shouldn’t be a problem.  I’m sure by 2038 you’d be hard-pressed to find a toaster without a 64-bit processor.  In JavaScript, a similar approach is taken, but it is the number of milliseconds since 1970-01-01 00:00:00 and I believe it has always used 64-bit integers (or, rather, something roughly equivalent), so nothing to worry about there.  Of special note though is that JavaScript assumes the value to specifically be the number of milliseconds since 1970-01-01 00:00:00 UTC. Those last three letters will become important later.

Basic Delphi Usage.

Using TDateTime within Delphi is not particularly difficult and likely something you’re already familiar with, so we’ll not spend too much time on it.  Fortunately, this all works just fine in TMS WEB Core as well.  Here are a bunch of examples of the ways I use TDateTime most often. These are the kinds of things we’ll want to be able to do in JavaScript as well. And while some of this might seem like a big steaming pile of headache, in practice this isn’t the case.  Most of the time datepickers are used to pick the actual dates from a calendar, or other UI elements are used, to make much of this a non-issue.  And you typically wouldn’t be using all of these at the same time!

...
uses
...
System.DateUtils,
...
procedure TForm1.WebButton2Click(Sender: TObject);

  procedure TestTDateTime(aDateTime: TDateTime);
  begin
    console.log('TDateTime Value: '+FloatToStrF(ADateTime,ffNumber,5,3));
    console.log('TDateTime = 0.0: '+DateTimeToStr(0.0));

    console.log('Favourite DateTime Format: '        +FormatDateTime('yyyy-mm-dd hh:nn:ss', aDateTime));
    console.log('Second Favourite DateTime Format: ' +FormatDateTime('yyyy-mmm-dd hh:nn:ss', aDateTime));
    console.log('Third Favourite DateTime Format: '  +FormatDateTime('yyyy-mmm-dd (ddd) hh:nn:ss', aDateTime));

    console.log('Short DateTime Format: ' +FormatDateTime('ddddd t', aDateTime));
    console.log('Long DateTime Format: '  +FormatDateTime('ddddd tt', aDateTime));

    console.log('Short Date Format Settings: ' +FormatSettings.ShortDateFormat);
    console.log('Long Date Format Settings: '  +FormatSettings.LongDateFormat);
    console.log('Date Separator Settings: '    +FormatSettings.DateSeparator);
    console.log('Short Time Format Settings: ' +FormatSettings.ShortTimeFormat);
    console.log('Long Time Format Settings: '  +FormatSettings.LongTimeFormat);
    console.log('Time Separator Settings: '    +FormatSettings.TimeSeparator);

    console.log('First Day of Month: '       +FormatDateTime('yyyy-mmm-dd', StartOfTheMonth(ADateTime)));
    console.log('Last Day of Month: '        +FormatDateTime('yyyy-mmm-dd', EndOfTheMonth(ADateTime)));
    console.log('First Day of Prior Month: ' +FormatDateTime('yyyy-mmm-dd', StartOfTheMonth(IncMonth(ADateTime,-1))));
    console.log('Last Day of Prior Month: '  +FormatDateTime('yyyy-mmm-dd', EndOfTheMonth(IncMonth(ADateTime,-1))));
    console.log('First Day of Next Month: '  +FormatDateTime('yyyy-mmm-dd', StartOfTheMonth(IncMonth(ADateTime,+1))));
    console.log('Last Day of Next Month: '   +FormatDateTime('yyyy-mmm-dd', EndOfTheMonth(IncMonth(ADateTime,+1))));

    console.log('Day of Year: '            +IntToStr(DayOfTheYear(aDateTime)));
    console.log('Day of Week: '            +FormatDateTime('dddd', ADateTime));

    // Week starts on Monday
    console.log('ISO Day of Week Number: '      +IntToStr(DayoftheWeek(ADateTime)));
    console.log('ISO Week Number: '             +IntToStr(WeekOfTheYear(aDateTime)));
    console.log('ISO First Day of Week: '       +FormatDateTime('yyyy-mmm-dd (ddd)',StartOfTheWeek(aDateTime)));
    console.log('ISO Last Day of Week: '        +FormatDateTime('yyyy-mmm-dd (ddd)',EndOfTheWeek(aDateTime)));
    console.log('ISO First Day of Prior Week: ' +FormatDateTime('yyyy-mmm-dd (ddd)',StartOfTheWeek(aDateTime-7)));
    console.log('ISO Last Day of Prior Week: '  +FormatDateTime('yyyy-mmm-dd (ddd)',EndOfTheWeek(aDateTime-7)));
    console.log('ISO First Day of Next Week: '  +FormatDateTime('yyyy-mmm-dd (ddd)',StartOfTheWeek(aDateTime+7)));
    console.log('ISO Last Day of Next Week: '   +FormatDateTime('yyyy-mmm-dd (ddd)',EndOfTheWeek(aDateTime+7)));

    // Week starts on Sunday
    console.log('non-ISO Day of Week Number: '      +IntToStr(DayofWeek(ADateTime)));
    // If Sunday, use Monday instead because WeekOfTheYear doesn't know about Sunday weeks
    console.log('non-ISO Week Number: '             +IntToStr(WeekOfTheYear(aDateTime+Trunc(DayOftheWeek(ADateTime)/7))));
    console.log('non-ISO First Day of Week: '       +FormatDateTime('yyyy-mmm-dd (ddd)',aDateTime + 1 - DayOfWeek(aDateTime)));
    console.log('non-ISO Last Day of Week: '        +FormatDateTime('yyyy-mmm-dd (ddd)',aDateTime + 7 - DayOfWeek(aDateTime)));
    console.log('non-ISO First Day of Prior Week: ' +FormatDateTime('yyyy-mmm-dd (ddd)',aDateTime - 6 - DayOfWeek(aDateTime)));
    console.log('non-ISO Last Day of Prior Week: '  +FormatDateTime('yyyy-mmm-dd (ddd)',aDateTime     - DayOfWeek(aDateTime)));
    console.log('non-ISO First Day of Next Week: '  +FormatDateTime('yyyy-mmm-dd (ddd)',aDateTime + 8 - DayOfWeek(aDateTime)));
    console.log('non-ISO Last Day of Next Week: '   +FormatDateTime('yyyy-mmm-dd (ddd)',aDateTime +14 - DayOfWeek(aDateTime)));

    console.log('Calculating a day duration: '  +IntToStr(DaysBetween(Today,EncodeDate(1971,05,25))));
    console.log('Calculating a time duration: ' +IntToStr(MillisecondsBetween(Now,Now-1))+'ms');
  end;

begin
  TestTDateTime(EncodeDateTime(2021, 12, 26, 12, 13, 14, 0)); // A Sunday
  TestTDateTime(EncodeDateTime(2022,  1,  2, 12, 13, 14, 0)); // Also a Sunday
  TestTDateTime(EncodeDateTime(2022,  1,  9, 12, 13, 14, 0)); // Yep, another Sunday
end;

Gathering up the results of the three dates and putting them into a table gets us the following.  We’re making a distinction here between weeks that start on Monday (the ISO standard) and weeks that start on Sunday (typically Canada and the USA, but others as well).  We’ll address that in just a moment, but the main thing to keep in mind is that this changes where Sunday falls in terms of the actual week number.  If you don’t use week numbers, this is a non-issue.  If you do, this is, well, this is not a non-issue at all.

2021-Dec-26 (Sunday)

2022-Jan-02 (Sunday)

2022-Jan-09 (Sunday)

TDateTime Value

44,556.509

44,563.509

44,570.509

TDateTime = 0.0

1899-12-30

1899-12-30

1899-12-30

Favourite DateTime Format

2021-12-26 12:13:14

2022-01-02 12:13:14

2022-01-09 12:13:14

Second Favourite DateTime Format

2021-Dec-26 12:13:14

2022-Jan-02 12:13:14

2022-Jan-09 12:13:14

Third Favourite DateTime Format

2021-Dec-26 (Sun) 12:13:14

2022-Jan-02 (Sun) 12:13:14

2022-Jan-09 (Sun) 12:13:14

Short DateTime Format

2021-12-26 12:13

2022-01-02 12:13

2022-01-09 12:13

Long DateTime Format

2021-12-26 12:13:14

2022-01-02 12:13:14

2022-01-09 12:13:14

Short Date Format Settings

yyyy-mm-dd

yyyy-mm-dd

yyyy-mm-dd

Long Date Format Settings

ddd, yyyy-mm-dd

ddd, yyyy-mm-dd

ddd, yyyy-mm-dd

Date Separator Settings

Short Time Format Settings

hh:nn

hh:nn

hh:nn

Long Time Format Settings

hh:nn:ss

hh:nn:ss

hh:nn:ss

Time Separator Settings

:

:

:

First Day of Month

2021-Dec-01

2022-Jan-01

2022-Jan-01

Last Day of Month

2021-Dec-31

2022-Jan-31

2022-Jan-31

First Day of Prior Month

2021-Nov-01

2021-Dec-01

2021-Dec-01

Last Day of Prior Month

2021-Nov-30

2021-Dec-31

2021-Dec-31

First Day of Next Month

2022-Jan-01

2022-Feb-01

2022-Feb-01

Last Day of Next Month

2022-Jan-31

2022-Feb-28

2022-Feb-28

Day of Year

360

2

9

Day of Week

Sunday

Sunday

Sunday

ISO Day of Week Number

7

7

7

ISO Week Number

51

52

1

ISO First Day of Week

2021-Dec-20 (Mon)

2021-Dec-27 (Mon)

2022-Jan-03 (Mon)

ISO Last Day of Week

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

ISO First Day of Prior Week

2021-Dec-13 (Mon)

2021-Dec-20 (Mon)

2021-Dec-27 (Mon)

ISO Last Day of Prior Week

2021-Dec-19 (Sun)

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

ISO First Day of Next Week

2021-Dec-27 (Mon)

2022-Jan-03 (Mon)

2022-Jan-10 (Mon)

ISO Last Day of Next Week

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

2022-Jan-16 (Sun)

non-ISO Day of Week Number

1

1

1

non-ISO Week Number

52

1

2

non-ISO First Day of This Week

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

non-ISO Last day of This Week

2022-Jan-01 (Sat)

2022-Jan-08 (Sat)

2022-Jan-15 (Sat)

non-ISO First Day of Prior Week

2021-Dec-19 (Sun)

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

non-ISO Last Day of Prior Week

2022-Jan-01 (Sat)

2022-Jan-01 (Sat)

2022-Jan-08 (Sat)

non-ISO First Day of Next Week

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

2022-Jan-16 (Sun)

non-ISO Last Day of Next Week

2022-Jan-08 (Sat)

2022-Jan-15 (Sat)

2022-Jan-22 (Sat)

Calculating a day duration

18626

18626

18626

non-ISO Last day of next Week

86400000ms

86400000ms

86400000ms

There are many more functions available, of course, and your favorites may very well be different from mine, naturally.  But even from this very basic list of function calls,  quite a few topics of interest arise that we should probably cover before continuing on. But a general disclaimer here.  There are various conventions used throughout the world when it comes to using and displaying dates and times.  What is customary where I live may very well be different in another part of the world.  And people have opinions, sometimes very strongly-held opinions, myself included. And I can’t think of anything of the top of my head that any two developers from different regions of the world might disagree on more strongly than something related to date formats. The main overall point I’m trying to make here is that there are options, and as TMS WEB Core developers (or any developers, really) we have a lot of tools at our disposal to make this kind of thing a pleasure for our users or customers, no matter what their particular preferred conventions might be, rather than a punishment.  So here goes!

  1. Regardless of development platform or programming language, it is generally considered good practice to always use the available functions provided by your environment when dealing with datetime values.  In part, this is to save you from the headaches that come with having to think about leap years or how many days are in a month.  This is also working with the assumption that system_function(a_valid_date) -> another_valid_date.  Numerous errors can creep in when you start fiddling with things. This is not always easy though, and the further you stray from “convention” the more likely you are to run into problems.  Even in my examples above, there’s more fiddling than I’d like with those first/last day of week calculations. Fortunately, I’ve not yet run across anyone who has argued against weeks having seven days, so this is reasonably safe. One of the few instances where there aren’t numerous opinions.
  2. Speaking of which, ISO8601 seems at first to be a strict standard in many respects, but on closer inspection it isn’t really all that strict at all. Except, curiously, for the start of the week, which it unequivocally states is Monday.  However, in my part of the world (Canada) I honestly can’t recall ever seeing a printed calendar that didn’t have Sunday on the left and Saturday on the right.  Which means, indirectly, the week starts on Sunday.  And everywhere I’ve worked, and everyone I’ve worked with, would likely have the same opinion.  Yet, at another location, perhaps in another country or continent, the exact opposite may be the case. And while I’ve never run across this situation personally, I’ve read that in some places the first day of the week can be Saturday.  More specifically, some ethnic groups (regardless of physical location) may prefer this. So…. ??!  This can be a real problem. Delphi itself even has different variations of its functions.  One set of variations offers up the ability of passing in TDateTime values instead of the constituent parts (the “the” functions).  Another variation relates to whether Sunday = 1 or Monday = 1.  Tricky, tricky.  Whatever situation you’re faced with, there’s a need to be careful about this and be sure that you use the same functions (and conventions) consistently, if only for your own sanity.
  3. Similarly, week numbers can sometimes be a cause for confusion. A large number of years ago, I remember visiting an office in early January when staff were checking out the latest swag from two separate billion-dollar-plus agriculture vendors, both from the same northern-European country that rather prides itself in its agricultural prowess.  The swag in this case included very large wall calendars, showing the entire year at a glance.  With equal parts horror and humor, it turned out that the calendars from the different vendors didn’t have the same week numbers assigned.  I think, collectively as a planet, we’ve since come to agree that Week #1 is considered the first week that has a Thursday in it.  ISO8601 prevails. Though this is more of a “recent” realization that people have arrived at, and isn’t necessarily implemented the same way everywhere. That’s what we’re going with here though.  Conveniently, this isn’t really in conflict really with my point (2) above. We’ll just have to argue about which week Sundays fall into. 
  4. TDateTime has millisecond precision, if you’re interested in that level of detail. However, some systems (databases, for example) may have more precision.  DB2 timestamps record microseconds (six digits instead of three), as do others. So this is something to be mindful of when moving data around, particularly if you’re comparing dates looking for exact matches.  And especially when the database itself generates timestamps, which we’ll cover in more detail shortly. Sometimes this also happens behind the scenes. This is why, if you’re using conventional Delphi DB components to access a database, timestamps are truncated after seconds.  To ensure that the components and the database are always using the same precision.
  5. An example of ISO8601 being perhaps less than rigid is that dates can be expressed as either YYYY-MM-DD or YYYYMMDD – what they refer to as enhanced vs. basic.  Nothing like a good standard to not take sides!  So when there is a claim that something is ISO8601 compliant, you have to really not put too much faith in that alone as it may mean different things to different people.  And yes, I’m looking at you, TXDataWebDataset.SetJSONData. Or FireDAC, equally to blame I suppose! As with anything else, these sorts of things have to be tested and you have to keep an eye out not only for variations in formats but also that when things are working, that they aren’t causing other problems downstream. One part of a series of function calls may happily tolerate slight variations in a format whereas another will be stopped cold.
  6. Users have preferences.  Customers have preferences.  Developers have preferences.  Rarely are these in alignment. And sometimes the deciding factor isn’t related to standards or coding or any of these preferences, but rather the dreaded “business rules” when someone decided, likely decades ago, that “This is the way” for that organization. It is for this reason that I’ve gotten into the habit of providing a general configuration option in my projects, where different periods (weeks, months, fiscal years, quarters, seasons, payroll periods, stat holidays, etc.) can be arbitrarily defined and then deployed consistently across an organization, at least within the confines of my projects. At least when they mess it up (which they also consistently do, nearly every year!), it is messed up consistently for everyone. Not sure if that has been a net benefit or not?  Point being, though, that with a little (OK, a LOT of) luck your users or your customers will hopefully be internally consistent with this kind of thing, at least within their own organization.
  7. Then there is the concept of “internationalization”.  Let’s call it that for now, anyway.  Sometimes it’s called “localization”.  Not confusing at all.  The idea that wherever you are in the world, your region (typically your country) has some generally-agreed-upon ideas about what the date format should be, what the first day of the week should be, what the names of the days of the week and months of the year are (with variations for official languages in the region), the equivalent shorter names for days of the week and months of the year, how “medium” and “long” dates should be expressed, and so on.  Often you’ll see software settings in applications where you can select a different region, if you happen to find your own region’s preferred default settings aren’t to your liking. I suspect Denmark is likely a more popular regional selection than it otherwise would be, for this very reason. One (very good!) school of thought is that as a developer you should use this system exclusively, giving good defaults to all users globally, with the option for them to use different settings if they choose.  Noble even, perhaps.  But perhaps misplaced.
  8. And finally, completely contrary to (7) above, is that users in a particular region may not wish to use the defaults for their region, or any region. Even if they happen to live in Denmark! And this system is certainly not without flaws of its own. For example, I regularly use a Fedora desktop system with FireFox, Thunderbird (BetterBird, actually), and Google Chrome.  It seems they all (sort of) rely on the Fedora locale to do their thing.  Which Fedora itself does a very poor job of in terms of offering any kind of flexibility. And thus everything downstream from there gets progressively worse.  Even Chrome and Firefox may not agree on the particular options using the same system, which can be infuriating at times.

So, just to be clear, there are standards like ISO8601 that provide solid guidance on many things, but there are also routine exceptions to be aware of, and often rules that come into play that have no basis in reality.  The good news is that the tools on-hand are easily capable of handling pretty much anything, so long as someone can nail down an unambiguous definition. And stick to it. Not as common as you might think!

As a final example of all this silliness, the FlatPickr JS Library we’ve just covered doesn’t list in its options anything to do with when the week starts. Presumably it picks up the settings from the local system information.  Or at least that’s what I hope it does.  However, what if it is displaying the calendar starting with Sunday on the left, but you’d really actually rather it always start with Monday?  Or perhaps the reverse is happening?  Turns out there is a way to do that, with just a bit more fiddling.  The solution can be found in the FlatPickr documentation, under Localization. Other related functionality can also be found there. 
procedure TForm1.WebFormCreate(Sender: TObject);
begin
  asm

    var fp1 = flatpickr('#WebEdit1', {
      appendTo: WebHTMLDiv1,
      inline: true,
      weekNumbers: true,
      locale: {
        firstDayOfWeek:0, // Sunday
      }
    });

    var fp2 = flatpickr('#WebEdit2', {
      appendTo: WebHTMLDiv2,
      inline: true,
      weekNumbers: true,
      locale: {
        firstDayOfWeek:1, // Monday
      }
    });

  end;
end;

 

TMS Software Delphi  Components
I’ll update the JSExtend package shortly to have this as an option in the IDE as well. But here’s a better visual representation of why I chose the three dates I did for the example.  We really do want this all to be bullet-proof and unambiguous, but it can be a struggle at times. And we’re not done with internationalization/localization, but we’ll set it aside for the moment and deal with another problematic issue.

Timezones, UTC, Local Time, and Offsets.

This topic is a little simpler and at the same time a little more complex.  The simpler part, believe it or not, is that everyone agrees (more or less, ha!) on what the timezones are, and where the physical boundaries are, when the time changes take place and so on.  And even if the boundaries are sometimes vague, the user likely knows what timezone they are in and their system (browser, OS, etc.) likely has something in it that will help us to identify the timezone without any kind of ambiguity.  Generally you set your timezone (or, more likely, just confirm your timezone) when first setting up your computer or phone or browser or whatever you’re using, and that’s it – a one time thing that you don’t really have to think about anymore, thankfully. Phones usually don’t even need to ask because they can get that from the carrier. 

It has been a long time coming! But I think we’re there now. Timezone data does change, however, so this is one of those things that comes up that requires periodic updates to software.  The idea that you can create an application and walk away and never update it again is more than a little old-fashioned. Timezone changes may require updates to the underlying OS, or perhaps the libraries you’re using in your project, the browser, or even your own code (let’s hope not!), or even all of the above.  For our purposes, we’ll assume that we’re using the latest version of everything, and that we can blame the browser vendor if an issue ever arises.  Just kidding. Mostly.

The more complex part is that Delphi assumes we know nothing about timezones.  And JavaScript assumes we know everything about timezones.  So there’s a bit of an information gap to overcome when moving back and forth between the two. Well, TDateTime assumes we know nothing about timezones. But what are the bits that we really need to know about?  Perhaps the most critical bit of information is our local offset from UTC.  If we know that, we can do some critical things right away, like convert back and forth between local time and UTC.  Curiously, the TMS WEB Core source uses this to determine the local time offset from UTC:

Function GetLocalTimeOffset : Integer;
begin
  Result:=TJSDate.New.getTimezoneOffset();
end;

Which essentially means that it’s getting the offset from JavaScript.  This makes sense, as it is really only the browser that knows what is going on in a running TMS WEB Core application, so that’s the only place to get it. The browser itself will get it from whatever environment it is running in, so that should work fine. The Delphi VCL is a bit more comprehensive, with support for things like TTimeZone which doesn’t (yet) work in TMS WEB Core, but as we can’t rely on Windows (could we ever?) we have to get this information from the new definitive source – the browser. We can actually get a little bit more information from the browser, but things start to get a bit dicey in terms of whether a browser supports these calls, whether different platforms support these calls, whether that phone from 2014 supports these calls, and so on.  This may not always work.  But worth a shot.  Here’s what we’ve got.  Not a bad start.

procedure TForm1.WebButton1Click(Sender: TObject);
var
  tz_name: String;
  tz_short: String;
begin

  asm
    tz_name = Intl.DateTimeFormat().resolvedOptions().timeZone;
    tz_short = new Date().toLocaleString('en', {timeZoneName:'short'}).split(' ').pop();
  end;

  console.log('Local Time: '+FormatDateTime('yyyy-mmm-dd (ddd) hh:nn:ss', Now));
  console.log('Local Timezone Name: '+tz_name);
  console.log('Local Timezone Short: '+tz_short);
  console.log('UTC Offset: '+IntToStr(TJSDate.new.gettimezoneoffset())+' minutes');
  console.log('UTC: '+FormatDateTime('yyyy-mmm-dd (ddd) hh:nn:ss', LocalTimeToUniversal(Now)));
  console.log('UTC to Local Time: '+FormatDateTime('yyyy-mmm-dd (ddd) hh:nn:ss',UniversalTimeToLocal(EncodeDateTime(2022,5,22,6,0,0,0))));
end;
// console.log output:
Local Time: 2022-May-21 (Sat) 22:40:14
Local Timezone Name: America/Vancouver
Local Timezone Short: PDT
UTC Offset: 420 minutes
UTC: 2022-May-22 (Sun) 05:40:14
UTC to Local Time: 2022-May-21 (Sat) 23:00:00

Unfortunately, that’s about all that we can determine easily. Converting between timezones isn’t really an option so far, for example.  

Server-Side Sidebar.

When working with the client end of a client/server-type application, often the client really doesn’t care about timezones.  It’s not that they’re not important, but rather that most of the time, people either work in their own timezones, so all the offsets are always the same, or if they work with data from different timezones, it is still the local time that is important – no conversion is necessary. 

 

For example, I’ve spent a considerable amount of time working on time and attendance systems for commercial greenhouse operators (tomato and pepper growers mostly). With these systems, workers “punch” at a client kiosk computer throughout their workday, which then records the dates and times and activities in a local server database (the server records the time, it doesn’t trust the client).  Supervisors then use this data to monitor worker performance, being mindful that their times have to fall within assigned shift times, and meet various criteria based on the duration of activities that are being punched, the type of plants they are working on, and so forth. Workers at another location (in another timezone) working for the same company might very well punch at another client kiosk computer connected to another local database.  The HR administrator (working in yet another timezone) might coalesce all this data to process the organization’s weekly payroll. That person doesn’t necessarily much care where the work was done and has no need to convert the local times in the remote locations to any particular timezone.  The administrator just sees that workers punch in at, say, 06:00, and punch out at 14:00 so they get paid for that period of time (adjusted for paid and unpaid breaks, performance bonuses and so on). There is no need at all to be concerned with timezones.

On the other hand, if you have a server that is logging all kinds of activities to a local database from users in multiple timezones, the situation could be quite different.  An XData server might, for example, log connections and service request tickets to a local database.  That database might be configured to record timestamps using UTC or it might be configured to use the database server’s local time (which could even be different from the XData server’s timezone).  When a user submits a service request ticket using an XData server endpoint, the database server might record the time in its own timezone.  When the user wants to check up on that ticket, however, the time returned to the user needs to be converted back to their timezone. From their perspective, the timezone is irrelevant, but on the server, the times recorded for all tickets will have timestamps recorded in the database server timezone that reflect the actual order the tickets were created, regardless of the timezone of the persons creating the ticket.

How can we do this without having to store the user’s timezone in the database?  One approach is to do the conversion only when the user makes a subsequent request.  When a service operation is invoked that needs this information, like a “ticket info” request or a user-level report of some kind, the client timezone can be sent to XData, either as an endpoint parameter, or even just included as an element in a JWT. Then, when the database SQL is executed, we pass in this information to do the conversion.  The query result then has data in the corrected timezone. 

In DB2, such a conversion might look like this.  The server’s timezone and the user’s timezone are passed as query parameters (eg: America/Vancouver).

select
  timezone(TICKETTIMESTAMP, :SERVERTZ, :LOCALTZ) TICKETLOCALTIMESTAMP
from
  TICKETS
where
  USER='frank'

 

In MySQL (or MariaDB) the same approach can be used but with a diffrent function call.

select
  convert_tz(TICKETTIMESTAMP, :SERVERTZ, :LOCALTZ) TICKETLOCALTIMESTAMP
from
  TICKETS
where
  USER='frank'

Note that in both databases, some initial prep work likely needs to be done just to ensure that the databases have current timezone information. And while I’m not certain that every database has this same level of timezone support, I’d imagine that if these two do, many others do as well.

What if you need to do something similar server-side, but outside of a database?  Let’s say, for example, that you want to include the current timestamp on a report generated on the server, but presented to the user in their local timezone. One way is to make use of the very impressive TZDB.pas which contains the entire (current!) IANA Time Zone Database.  Simply download it and add it to your project, and then to your uses clause and you’re off and running.  The GitHub page has a ton of information on all the things it can do, and it is pretty extensive.  Here’s what you might do to timestamp a report.
function MyService.GetReport(Parameters:String):TStream;
var
  ClientTimeZone: TBundledTimeZone;
  ServerTime:TDateTime;
  ClientTime:TDateTime;
  GlobalTime:TDateTime;
  TimeZone: String;
begin
...
// TimeZone value is retrieved from the JWT
...
  ClientTimeZone :=TBundledTimeZone.GetTimeZone(TimeZone);
  ServerTime := Now;
  GlobalTime := TTimeZone.Local.ToUniversalTime(ServerTime);
  ClientTime := ClientTimeZone.ToLocalTime(GlobalTime);
  Report.FooterLeft.Caption := FormatDateTime('yyyy-mmm-dd (ddd) hh:nn:ss', ClientTime);
...
end;

This same approach could be used to pass locale information or other datetime formatting-related parameters.  Which would also be good candidates to store in the JWT.  Not likely someone is going to change languages frequently, or their preferred date format, during their session. Or if they did, it would be a reasonable justification to generate a new JWT anyway.

Luxon Enters the Chat.

Alright.  Sidebar over.  Back at the client now, we’ve dealt with quite a lot of things from the Delphi side, so lets dig into the JavaScript side a little further. What I’d like to start with is the JavaScript equivalent of the examples I provided for Delphi.  Sure would make sense to start there, wouldn’t it?  But alas, we get to the third entry where we want to format the date output and immediately get stuck.  Turns out there’s no real simple way in native JavaScript to do the same thing that the Delphi FormatDateTime function does.  But wait just a second!  I know what you’re thinking.  TMS WEB Core converted FormatDateTime to JavaScript and it worked just fine, right?  Sure, that’s exactly what I thought.  So I went and checked out what they did. Most impressive! I really encourage you to go and have a look for yourself.  …Core SourceSystem.SysUtils.pas. So… yeah.  I’d like to take a different approach here.

For many years, by far the popular JS Library for dealing with all of this kind of thing was moment.js. But recently (2021), the creators of that project announced that it had reached its end-of-life and that it was no longer going to be actively developed.  They gave two primary reasons.  One was that it had grown substantially, adding potentially more than 200KB to projects, and in a way that was not particularly amenable to modern JavaScript optimization techniques like “tree-shaking.” The other was this idea about immutability – using the library meant using particular coding styles, or else you risked unintended side-effects that weren’t always obvious.  I’m not particularly clear on this last point or how to define it in terms of a Delphi equivalent.  This seems more of a JavaScript language kind of problem, but they felt that it was problematic enough to just call it quits. One of the alternatives that they recommend, that is roughly on-par feature-wise with their project, is Luxon.  Its main claims are that it is smaller than moment.js and that its objects are immutable. As the heir-apparent to moment.js, it seems to have attracted quite a following.  And part of the reason why I’ve chosen to feature it here is that it is also the library that supports this kind of functionality within Tabulator, which is soon to be featured in our blog series.

To get started with Luxon, we have the usual requirement to add a link to the Project.html file, either manually or via the Manage JavaScript Libraries feature.  I’d suggest that this is a highly-used library and unlikely to be updated with breaking changes, so pretty safe, relatively-speaking, to use a CDN. Like this:

There are no visual objects or IDE-related things to show off here.  It is just a Helper JS Library that has a bunch of code that we can use.  We could do all this without Luxon but it would be quite a bit of work.  With Luxon on the job, we can then tackle converting our example from Delphi to JavaScript. The formatting tokens are naturally different, but you can find their list of tokens here. And while there is a lot to like in there, there are so many choices when it comes to what might be considered “localized” formats as to make them very nearly meaningless.  And somehow, on my system, things like AM/PM and M/D/YYYY start to show up, despite my very best efforts to avoid them.
procedure TForm1.WebButton4Click(Sender: TObject);
begin
  asm
    function testLuxon(aDate) {
      console.log('Date Value: '+aDate);

      console.log('Favourite DateTime Format: '        +aDate.toFormat('yyyy-LL-dd HH:mm:ss'));
      console.log('Second Favourite DateTime Format: ' +aDate.toFormat('yyyy-LLL-dd HH:mm:ss'));
      console.log('Third Favourite DateTime Format: '  +aDate.toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));

      console.log('Shortest DateTime Format: ' +aDate.toFormat('f'));
      console.log('Shorter DateTime Format: '  +aDate.toFormat('ff'));
      console.log('Shortish DateTime Format: ' +aDate.toFormat('fff'));
      console.log('Short DateTime Format: '    +aDate.toFormat('ffff'));
      console.log('Long DateTime Format: '     +aDate.toFormat('F'));
      console.log('Longish DateTime Format: '  +aDate.toFormat('FF'));
      console.log('Longer DateTime Format: '   +aDate.toFormat('FFF'));
      console.log('Longest DateTime Format: '  +aDate.toFormat('FFFF'));

      console.log('First Day of Month: '+aDate.startOf('month').toFormat('yyyy-LLL-dd'));
      console.log('Last Day of Month: '+aDate.endOf('month').toFormat('yyyy-LLL-dd'));
      console.log('First Day of Prior Month: '+aDate.plus({months: -1}).startOf('month').toFormat('yyyy-LLL-dd'));
      console.log('Last Day of Prior Month: '+aDate.plus({months: -1}).endOf('month').toFormat('yyyy-LLL-dd'));
      console.log('First Day of Next Month: '+aDate.plus({months: 1}).startOf('month').toFormat('yyyy-LLL-dd'));
      console.log('Last Day of Next Month: '+aDate.plus({months: 1}).endOf('month').toFormat('yyyy-LLL-dd'));

      console.log('Day of Week: ' +aDate.toFormat('cccc'));
      console.log('Day of Year: ' +aDate.toFormat('o'));

      // Start of Week = Monday
      console.log('ISO Day of Week Number: '      +aDate.weekday);
      console.log('ISO Week Number: '             +aDate.weekNumber);
      console.log('ISO First Day of Week: '       +aDate.startOf('week').toFormat('yyyy-LLL-dd (ccc)'));
      console.log('ISO Last Day of Week: '        +aDate.endOf('week').toFormat('yyyy-LLL-dd (ccc)'));
      console.log('ISO First Day of Prior Week: ' +aDate.plus({weeks: -1}).startOf('week').toFormat('yyyy-LLL-dd (ccc)'));
      console.log('ISO Last Day of Prior Week: '  +aDate.plus({weeks: -1}).endOf('week').toFormat('yyyy-LLL-dd (ccc)'));
      console.log('ISO First Day of Next Week: '  +aDate.plus({weeks: 1}).startOf('week').toFormat('yyyy-LLL-dd (ccc)'));
      console.log('ISO Last Day of Next Week: '   +aDate.plus({weeks: 1}).endOf('week').toFormat('yyyy-LLL-dd (ccc)'));

      // Start of Week = Sunday
      console.log('non-ISO Day of Week: '             +aDate.toFormat('cccc'));
      // Mon=1,Sun=7 >>> Sun=1, Sat=7
      console.log('non-ISO Day of Week Number: '      +((aDate.weekday % 7) + 1));
      // If Sunday, get weekNumber for Monday
      console.log('non-ISO Week Number: '             +aDate.plus({days: Math.trunc(aDate.weekday / 7)}).weekNumber);
      console.log('non-ISO Day of Year: '             +aDate.toFormat('o'));
      console.log('non-ISO First Day of Week: '       +aDate.plus({days: + 1 - ((aDate.weekday % 7) + 1)}).toFormat('yyyy-LLL-dd (ccc)'));
      console.log('non-ISO Last Day of Week: '        +aDate.plus({days: + 7 - ((aDate.weekday % 7) + 1)}).toFormat('yyyy-LLL-dd (ccc)'));
      console.log('non-ISO First Day of Prior Week: ' +aDate.plus({days: - 6 - ((aDate.weekday % 7) + 1)}).toFormat('yyyy-LLL-dd (ccc)'));
      console.log('non-ISO Last Day of Prior Week: '  +aDate.plus({days:     - ((aDate.weekday % 7) + 1)}).toFormat('yyyy-LLL-dd (ccc)'));
      console.log('non-ISO First Day of Next Week: '  +aDate.plus({days: + 8 - ((aDate.weekday % 7) + 1)}).toFormat('yyyy-LLL-dd (ccc)'));
      console.log('non-ISO Last Day of Next Week: '   +aDate.plus({days: +14 - ((aDate.weekday % 7) + 1)}).toFormat('yyyy-LLL-dd (ccc)'));

      console.log('Calculating a day duration: '+Math.trunc(luxon.DateTime.now().diff(luxon.DateTime.local(1971,5,25,0,0,0,0), 'days').days));
      console.log('Calculating a time duration: '+luxon.DateTime.now().diff(luxon.DateTime.now().plus({days: -1}))+'ms');
   }

    testLuxon(luxon.DateTime.local(2021, 12, 26, 12, 13, 14, 0));
    testLuxon(luxon.DateTime.local(2022,  1,  2, 12, 13, 14, 0));
    testLuxon(luxon.DateTime.local(2022,  1,  9, 12, 13, 14, 0));

  end;
end;

The end result is that we get the same table as before, with the exception of the various date formats near the beginning.  Note that if you run this in your own browser, the formats might look very different than this, but hopefully the rest of the table remains the same.  That is kind of the point, after all.

2021-Dec-26 (Sunday)

2022-Jan-02 (Sunday)

2022-Jan-09 (Sunday)

Date Value

1640549594000

1641154394000

1641759194000

Favourite DateTime Format

2021-12-26 12:13:14

2022-01-02 12:13:14

2022-01-09 12:13:14

Second Favourite DateTime Format

2021-Dec-26 12:13:14

2022-Jan-02 12:13:14

2022-Jan-09 12:13:14

Third Favourite DateTime Format

2021-Dec-26 (Sun) 12:13:14

2022-Jan-02 (Sun) 12:13:14

2022-Jan-09 (Sun) 12:13:14

Shortest DateTime Format

12/26/2021, 12:13 PM

1/2/2022, 12:13 PM

1/9/2022, 12:13 PM

Shorter DateTime Format

Dec 26, 2021, 12:13 PM

Jan 2, 2022, 12:13 PM

2022-01-09 12:13:14

Shortish DateTime Format

December 26, 2021, 12:13 PM PST

January 2, 2022, 12:13 PM PST

January 9, 2022, 12:13 PM PST

Short DateTime Format

Sunday, December 26, 2021, 12:13 PM Pacific Standard Time

Sunday, January 2, 2022, 12:13 PM Pacific Standard Time

Sunday, January 9, 2022, 12:13 PM Pacific Standard Time

Long DateTime Format

12/26/2021, 12:13:14 PM

1/2/2022, 12:13:14 PM

1/9/2022, 12:13:14 PM

Longish DateTime Format

Dec 26, 2021, 12:13:14 PM

Jan 2, 2022, 12:13:14 PM

Jan 9, 2022, 12:13:14 PM

Longer DateTime Format

December 26, 2021, 12:13:14 PM PST

January 2, 2022, 12:13:14 PM PST

January 9, 2022, 12:13:14 PM PST

Longest DateTime Format

Sunday, December 26, 2021, 12:13:14 PM Pacific Standard
Time

Sunday, January 2, 2022, 12:13:14 PM Pacific Standard Time

Sunday, January 9, 2022, 12:13:14 PM Pacific Standard Time

First Day of Month

2021-Dec-01

2022-Jan-01

2022-Jan-01

Last Day of Month

2021-Dec-31

2022-Jan-31

2022-Jan-31

First Day of Prior Month

2021-Nov-01

2021-Dec-01

2021-Dec-01

Last Day of Prior Month

2021-Nov-30

2021-Dec-31

2021-Dec-31

First Day of Next Month

2022-Jan-01

2022-Feb-01

2022-Feb-01

Last Day of Next Month

2022-Jan-31

2022-Feb-28

2022-Feb-28

Day of Year

360

2

9

Day of Week

Sunday

Sunday

Sunday

ISO Day of Week Number

7

7

7

ISO Week Number

51

52

1

ISO First Day of Week

2021-Dec-20 (Mon)

2021-Dec-27 (Mon)

2022-Jan-03 (Mon)

ISO Last Day of Week

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

ISO First Day of Prior Week

2021-Dec-13 (Mon)

2021-Dec-20 (Mon)

2021-Dec-27 (Mon)

ISO Last Day of Prior Week

2021-Dec-19 (Sun)

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

ISO First Day of Next Week

2021-Dec-27 (Mon)

2022-Jan-03 (Mon)

2022-Jan-10 (Mon)

ISO Last Day of Next Week

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

2022-Jan-16 (Sun)

non-ISO Day of Week Number

1

1

1

non-ISO Week Number

52

1

2

non-ISO First Day of This Week

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

non-ISO Last day of This Week

2022-Jan-01 (Sat)

2022-Jan-08 (Sat)

2022-Jan-15 (Sat)

non-ISO First Day of Prior Week

2021-Dec-19 (Sun)

2021-Dec-26 (Sun)

2022-Jan-02 (Sun)

non-ISO Last Day of Prior Week

2022-Jan-01 (Sat)

2022-Jan-01 (Sat)

2022-Jan-08 (Sat)

non-ISO First Day of Next Week

2022-Jan-02 (Sun)

2022-Jan-09 (Sun)

2022-Jan-16 (Sun)

non-ISO Last Day of Next Week

2022-Jan-08 (Sat)

2022-Jan-15 (Sat)

2022-Jan-22 (Sat)

Calculating a day duration

18626

18626

18626

non-ISO Last day of next Week

86400000ms

86400000ms

86400000ms

Alright.  So now we can do in JavaScript what we could already do in Delphi.  That’s important, certainly, so that we are able to check our work and make sure that things remain consistent.  However, we’re not here just to convert Delphi to JavaScript. Nor to remind myself that I’m old!  Rather, the point of all this is that we want to be able to leverage what Luxon can provide to further enhance and integrate with our Delphi and TMS WEB Core projects.  

Great. So What Else Can Luxon Do?

Quite a lot, actually.  First, converting between timezones is now not so difficult at all.

asm
 var vancouver = luxon.DateTime.local(2022, 5, 22, 12, 13, 14, 0);
 console.log('America/Vancouver: '+vancouver.toFormat('yyyy-LLL-dd (ccc) HH:mm:ss')+' ( timezone = '+vancouver.zoneName+' )');
 console.log('America/New_York: '+vancouver.setZone('America/New_York').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
 console.log('Europe/London: '+vancouver.setZone('Europe/London').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
 console.log('Europe/Paris: '+vancouver.setZone('Europe/Paris').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
 console.log('Europe/Kiev: '+vancouver.setZone('Europe/Kiev').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
 console.log('Asia/Singapore: '+vancouver.setZone('Asia/Singapore').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
end;
// console.log output:
America/Vancouver: 2022-May-22 (Sun) 12:13:14 ( timezone = America/Vancouver )
America/New_York: 2022-May-22 (Sun) 15:13:14
Europe/London: 2022-May-22 (Sun) 20:13:14
Europe/Paris: 2022-May-22 (Sun) 21:13:14
Europe/Kiev: 2022-May-22 (Sun) 22:13:14
Asia/Singapore: 2022-May-23 (Mon) 03:13:14

Different variations of how the timezone is expressed can also be used, if the continent/city variation isn’t to your liking.

asm
  console.log('America/Vancouver: '+vancouver.toFormat('yyyy-LLL-dd (ccc) HH:mm:ss')+' ( timezone = '+vancouver.zoneName+' )');
  console.log('UTC: '+vancouver.setZone('utc').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
  console.log('UTC+1: '+vancouver.setZone('utc+1').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
  console.log('CET (Central Europe): '+vancouver.setZone('cet').toFormat('yyyy-LLL-dd (ccc) HH:mm:ss'));
end;
// console.log output:
America/Vancouver: 2022-May-22 (Sun) 12:13:14 ( timezone = America/Vancouver )
UTC: 2022-May-22 (Sun) 19:13:14
UTC+1: 2022-May-22 (Sun) 20:13:14
CET (Central Europe): 2022-May-22 (Sun) 21:13:14

We’ve already seen plenty of examples of adjusting a date plus/minus some interval, like ‘days’, ‘weeks’, ‘months’ and so on.  When calculating the difference between two dates, there is the option of also returning a value that has these kinds of intervals, making it possible to have more friendly durations shown.

asm
  var start  = luxon.DateTime.local(2022, 5, 21,  0, 13, 14);
  var finish = luxon.DateTime.local(2022, 5, 22, 12, 13, 14);     // 36 hours later

  console.log('start was '+Math.trunc(finish.diff(start, 'minutes').minutes)+' minute(s) ago');
  console.log('start was '+Math.trunc(finish.diff(start, 'hours').hours)+' hour(s) ago');
  console.log('start was '+Math.trunc(finish.diff(start, 'days').days)+' day(s) ago');
  console.log('start was '+JSON.stringify(finish.diff(start, ['days','hours','minutes']).toObject()));
  console.log('start was '+finish.diff(start, ['days','hours']).toHuman()+' ago');
end;
start was 2160 minute(s) ago
start was 36 hour(s) ago
start was 1 day(s) ago
start was {"days":1,"hours":12,"minutes":0}
start was 1 day, 12 hours ago

Not hard to imagine all kinds of ways this could be used to present more human-readable dates.

Getting Around.

These kinds of functions can also be nicely encapsulated.  I’ve some ideas around creating some common functions that could go into our JSExtend library, via something like Luxon.pas.  Here are some contenders.  What functions do you think might be useful?

function TForm1.ConvertTimezone(aDateTime: TDateTime; TZ: String): TDateTime;
var
  ayear, amonth, aday, ahour, aminute, asecond, amillisecond: word;
begin
  DecodeDateTime(aDateTime, ayear, amonth, aday, ahour, aminute, asecond, amillisecond);
  asm
    var lDateTime = new luxon.DateTime.local(ayear, amonth, aday, ahour, aminute, asecond, amillisecond).setZone(TZ);
    ayear = lDateTime.year;
    amonth = lDateTime.month;
    aday = lDateTime.day;
    ahour = lDateTime.hour;
    aminute = lDateTime.minute;
    asecond = lDateTime.second;
    amillisecond = lDateTime.millisecond;
  end;
  Result := EncodeDateTime(ayear, amonth, aday, ahour, aminute, asecond, amillisecond);
end;
procedure TForm1.WebButton1Click(Sender: TObject);
var
  olddate, newdate: TDateTime;
begin
  olddate := EncodeDateTime(2022, 5, 22, 12, 13, 14, 0);
  newdate := ConvertTimeZone(olddate, 'Europe/Paris');
  console.log(FormatDateTime('yyyy-mmm-dd hh:nn:ss',olddate));
  console.log(FormatDateTime('yyyy-mmm-dd hh:nn:ss',newdate));
end;
// console.log output:
2022-May-22 12:13:14
2022-May-22 21:13:14

function TForm1.HumanDifference(nowDateTime: TDateTime; thenDateTime: TDateTime): String;
var
  ayear, amonth, aday, ahour, aminute, asecond, amillisecond: word;
  byear, bmonth, bday, bhour, bminute, bsecond, bmillisecond: word;
begin
  DecodeDateTime(nowDateTime, ayear, amonth, aday, ahour, aminute, asecond, amillisecond);
  DecodeDateTime(thenDateTime, byear, bmonth, bday, bhour, bminute, bsecond, bmillisecond);
  asm
    var aDateTime = new luxon.DateTime.local(ayear, amonth, aday, ahour, aminute, asecond, amillisecond);
    var bDateTime = new luxon.DateTime.local(byear, bmonth, bday, bhour, bminute, bsecond, bmillisecond);
    Result =  aDateTime.diff(bDateTime, ['days','hours']).toHuman()+' ago';
  end;
end;
procedure TForm1.WebButton1Click(Sender: TObject);
var
  olddate, newdate: TDateTime;
begin
  olddate := EncodeDateTime(2022, 5, 21, 06, 13, 14, 0);
  newdate := EncodeDateTime(2022, 5, 22, 12, 13, 14, 0); // 30 hours later
  console.log(HumanDifference(newdate, olddate));
end;
// console.log output:
1 day, 6 hours ago

Out of Time!

I think that about covers at least the very basics of Luxon and using it to help enhance your TMS WEB Core projects.  I do hope you’ve found this useful, and that you’re able to easily take advantage of Luxon in your own projects, with whatever localization/internationalization/numbering systems that happen to come your way! 

Andrew Simard.