Running javascript library from Applescript

I want to use the javascript library moment.js from an AppleScript to get formatted date-time values.
Is there a good way to run such libraries from AppleScript, using AsObjC?
I have zero skills in ObjC, so I would appreciate any help with such code.

Note: I have tried to use the script posted by Shane in this thread but the main function of moment.js is not an ordinary named function.

The best way to run them from osascript is to use the -l JavaScript option.

See:

[Exotic Recipes · JXA-Cookbook/JXA-Cookbook Wiki](https://github.com/JXA-Cookbook/JXA-Cookbook/wiki/Exotic-Recipes#requiring-commonjs-and-npm-modules-using-browserify)

I think that you can only call ordinary functions from outside.
The easiest way would be to save the library as Moments.scpt and insert a function like this:

function doEval(s){
	return eval(s);
}

Than you can use the script posted by Shane:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "OSAKit"
use scripting additions
property myJSLib : missing value

try
	script "Moments.scpt"
on error from jsLib
	set jsLibPath to POSIX path of (jsLib as alias)
	set {my myJSLib, theError} to current application's OSAScript's alloc()'s initWithContentsOfURL:(current application's |NSURL|'s fileURLWithPath:jsLibPath) |error|:(reference)
	if myJSLib is missing value then error (theError's localizedDescription()) as text
end try

my callFunction:"doEval" withArgs:{"moment().format('MMMM Do YYYY, h:mm:ss a')"} inJSLib:myJSLib
--> October 23rd 2020, 9:33:15 pm

on callFunction:funcName withArgs:argsList inJSLib:myJSLib
	set {theResult, theError} to myJSLib's executeHandlerWithName:funcName arguments:argsList |error|:(reference)
	if theResult is missing value then error (theError's localizedDescription()) as text
	-- hack to convert from descriptor to correct AS class
	return item 1 of ((current application's NSArray's arrayWithObject:theResult) as list)
end callFunction:withArgs:inJSLib:

Not nice, but it works.

Alternatively you can write a wrapper function for each function you want to use.

That seems a long way around. Why not use ASObjC to get them directly?

I would prefer the short way, if that means I could use the same standards for formatting input as with moment.js as specified here (and if I could get a little help the ASObjC).
I am currently working in a project that combines node.js and Indesign-scripting. The javascript guys use moment.js for date formatting and we want to use to the same standards for formatting input – like YYYY, MM, DD, ddd, dddd, w, ww, MMM, MMMM and so on – in all parts of the project, including my applescripts.
Preferably the input, besides the format string, would be a local date as an date object or a string like “2020-10-24”.

Thanks!
Before creating this topic I did som experiments with JXA, calling moment.js as a script library (renamed to moment.scpt), with the intention to run that JXA script from AppleScript – only with strange errors or script editor crashing as a result. So I understood things were more complicated.
Then I read about using osascript -l JavaScript with browserify.
Before doing any serious attempts along that way I wanted to check if there is a simpler way to do achieve the same goal without the need to install binaries and so on. My main goal in this specific case is (despite my topic headline) not to be able to use javascript libraires (even if that is an interesting question as well) – but just to be able to make formatted date strings using the same set of possible format input strings as with moment.js.
But if there is no simpler way, I will certainly go furter with osascript/browserify.

1 Like

They’re basically standard symbols. So:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set df to current application's NSDateFormatter's new()
df's setDateFormat:"HH:mm:ss' - 'yyyy/MM/dd"
set theString to (df's stringFromDate:(current date)) as text

That’s wonderful!
So I had the wrong impression those input strings were just something specific to moment.js. This saves me from trying to make an unnecessary complicated solution.
Thanks a lot, Shane!

A couple of them (L, LL, &c) look a bit odd, but most are from here:

Ok! Thanks for the link

With Shanes ASObjC code you could check the Unicode website…

I made it simple for you.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

(**
* [dateFormatterWithFormat:(string)]
*
* https://www.unicode.org/reports/tr35/tr35-dates.html
*)
set theDateFormat to {"GGGG", "YY", "YYYY", "QQ", "QQQ", "QQQQ", ¬
	"MM", "MMM", "MMMM", "W", "ww", "dd", "DD", "F", "E", "EEEE", ¬
	"e", "ee", "a", "B", "hh", "HH", "K", "KK", "mm", "ss", "z", "Z", ¬
	"ZZZZ", "O", "v", "vvvv", "V", "VV", "VVV", "VVVV", ¬
	"yyyy.MM.dd G 'at' HH:mm:ss zzz", ¬
	"EEE, MMM d, yyyy", ¬
	"h:mm a", ¬
	"h:mm a, zzzz", ¬
	"K:mm a, z", ¬
	"HH:mm:ss' - 'yyyy/MM/dd"}

-- ex1. weekOfYear
set weekOfYear to my dateFormatterWithFormat:"w"
-- ex2. timeOfDay
set timeOfDay to my dateFormatterWithFormat:"HH:mm:ss B"

repeat with i from 1 to (count theDateFormat)
	log (my dateFormatterWithFormat:(item i of theDateFormat))
end repeat

log "Week of Year: " & weekOfYear
log "Local time is: " & timeOfDay

on dateFormatterWithFormat:_dateFormat
	set formatter to current application's NSDateFormatter's new()
	formatter's setDateFormat:(_dateFormat as string)
	set theString to (formatter's stringFromDate:(current date)) as text
end dateFormatterWithFormat:
1 Like

Very nice! Tack Fredrik

This is a better version with more examples…

use framework "Foundation"
use scripting additions

(**
* Reference:
*	https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DataFormatting/DataFormatting.html
*
* https://developer.apple.com/documentation/foundation/nsdateformatter
*
* https://nsdateformatter.com
*)

(**
* [YEAR]
* y, 2008, Year no padding
* yy, 08, Year two digits (padding with a zero if necessary)
* yyyy 2008, Year minimum of four digits (padding with zeros if necessary)
*)

(** 
* [QUARTER]
* Q, 4, The quarter of the year. Use QQ if you want zero padding.
* QQQ, Q4, Quarter including "Q"
* QQQQ, 4th quarter, Quarter spelled out
*)

(**
* [MONTH]
* M, 12, The numeric month of the year. A single M will use '1' for January.
* MM, 12, The numeric month of the year. A double M will use '01' for January.
* MMM, Dec, The shorthand name of the month
* MMMM, December, Full name of the month
* MMMMM, D, Narrow name of the month
*)

(** 
* [DAY]
* d, 14, The day of the month. A single d will use 1 for January 1st.
* dd, 14, The day of the month. A double d will use 01 for January 1st.
* F, 3rd Tuesday in December, The day of week in the month
* E, Tue, The abbreviation for the day of the week
* EEEE, Tuesday, The wide name of the day of the week
* EEEEE, T, The narrow day of week
* EEEEEE, Tu, The short day of week
*)

(**
* [HOUR]
* h, 4, The 12-hour hour.
* hh, 04, The 12-hour hour padding with a zero if there is only 1 digit
* H, 16, The 24-hour hour.
* HH, 16, The 24-hour hour padding with a zero if there is only 1 digit.
* a, PM, AM / PM for 12-hour time formats
*)

(**
* [MINUTE]
* m, 35, The minute, with no padding for zeroes.
* mm, 35, The minute with zero padding.
*)

(**
* [SECOND]
* s, 8, The seconds, with no padding for zeroes.
* ss, 08, The seconds with zero padding.
* SSS, 123, The milliseconds.
*)

(**
* [TIME ZONE]
* zzz, CST, The 3 letter name of the time zone. Falls back to GMT-08:00 (hour offset)
* zzzz, Central Standard Time, The expanded time zone name, falls back to GMT-08:00
* ZZZZ, CST-06:00, Time zone with abbreviation and offset
* Z, -0600, RFC 822 GMT format. Can also match a literal Z for Zulu (UTC) time.
* ZZZZZ, -06:00, ISO 8601 time zone format
*)

-- Examples.
log (my dateFormatterWithFormat:"EEEE, MMM d, yyyy")
log (my dateFormatterWithFormat:"MM/dd/yyyy")
log (my dateFormatterWithFormat:"MM-dd-yyyy HH:mm")
log (my dateFormatterWithFormat:"MMM d, h:mm a")
log (my dateFormatterWithFormat:"MMMM yyyy")
log (my dateFormatterWithFormat:"MMM d, yyyy")
log (my dateFormatterWithFormat:"E,d MMM yyyy HH:mm:ss Z")
log (my dateFormatterWithFormat:"yyyy-MM-dd'T'HH:mm:ssZ")
log (my dateFormatterWithFormat:"dd.MM.yy")
log (my dateFormatterWithFormat:"HH:mm:ss")
log (my dateFormatterWithFormat:"MMMM dd, yyyy HH:mm")
log (my dateFormatterWithFormat:"HH:mm, zzzz")
log (my dateFormatterWithFormat:"VV, VVVV, zz, ZZZZ")



if ((my dateFormatterWithFormat:"F") = "1") then
	log (my dateFormatterWithFormat:"F'st' EEEE 'in the month'")
end if
if ((my dateFormatterWithFormat:"F") = "2") then
	log (my dateFormatterWithFormat:"F'nd' EEEE 'in the month'")
end if
if ((my dateFormatterWithFormat:"F") = "3") then
	log (my dateFormatterWithFormat:"F'rd' EEEE 'in the month'")
end if
if ((my dateFormatterWithFormat:"F") = "4") or (my dateFormatterWithFormat:"F") = "5" then
	log (my dateFormatterWithFormat:"F'th' EEEE 'in the month'")
end if

(**
* [Class]: NSDateFormatter
*	A formatter that converts between dates and their textual representations.
*)

(**
* [dateFormatterWithFormat:(string)]
*)
on dateFormatterWithFormat:_dateFormat
	set formatter to current application's NSDateFormatter's alloc()'s init()
	formatter's setDateFormat:(_dateFormat as string)
	set theString to (formatter's stringFromDate:(current date)) as text
end dateFormatterWithFormat:

log (my worldClockWithFormat:"yyyy-mm-dd ':' HH:mm:ss")

log (my worldClockWithFormat:"HH:mm" timeZone:{"America/New_York", "Europe/Paris", "Australia/Sydney"})

on worldClockWithFormat:_dateFormat timeZone:_timeZoneList
	set theList to {}
	repeat with i from 1 to (count _timeZoneList)
		set timeZoneName to item i of _timeZoneList
		set formatter to current application's NSDateFormatter's alloc()'s init()
		(formatter's setDateFormat:(_dateFormat as string))
		(formatter's setTimeZone:(current application's NSTimeZone's timeZoneWithName:(timeZoneName as string)))
		set theString to (formatter's stringFromDate:(current date)) as text
		copy item i of _timeZoneList & space & theString to end of theList
	end repeat
	return theList
end worldClockWithFormat:timeZone:

on worldClockWithFormat:_dateFormat
	set timeZoneName to choose from list (timeZoneNames() as list)
	set formatter to current application's NSDateFormatter's alloc()'s init()
	formatter's setDateFormat:(_dateFormat as string)
	formatter's setTimeZone:(current application's NSTimeZone's timeZoneWithName:(timeZoneName as string))
	set theString to timeZoneName & space & (formatter's stringFromDate:(current date)) as text
end worldClockWithFormat:

on timeZoneNames()
	set zoneNames to current application's NSTimeZone's knownTimeZoneNames()
	return zoneNames as list
end timeZoneNames
1 Like

Just be aware that if you supply a format that differs from the typical order used in your locale, there’s a possibility it will be not be completely honored. In such cases setting an appropriate locale is a good idea.

1 Like

The above script from @Fredrik71 demonstrates how to get date and time for different timezones. That useful!
But is it possible to adjust language in the same way – so that I can get names of months and weekdays in other languages than my system language?

You do that by setting a locale:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set df to current application's NSDateFormatter's new()
df's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:"ja")
df's setDateFormat:"EEEE, MMMM"
set theString to (df's stringFromDate:(current date)) as text
--> 水曜日, 10月
1 Like

Again: Thanks Shane!

example include NSLocale to choose from availableLocaleIdentifiers

my dateFormatterWithFormat:"MMMM dd, yyyy HH:mm"

on dateFormatterWithFormat:_dateFormat
	set formatter to current application's NSDateFormatter's alloc()'s init()
	set identifiers to current application's NSLocale's availableLocaleIdentifiers
	set theLocale to choose from list (identifiers as list)
	formatter's setLocale:(current application's NSLocale's localeWithLocaleIdentifier:(theLocale as string))
	formatter's setDateFormat:(_dateFormat as string)
	set theString to (formatter's stringFromDate:(current date)) as text
end dateFormatterWithFormat: