How to format a date using JScript

In the code below, TheSelFileDate is a date object, set to a date in the past, and I wanted that date as a string in the form YYMMdd. I partially solved the problem by using a non-script DOpus command, then going back into JScript:


ClickData.Func.Command.Vars.set ("TheSelFileDate",TheSelFileDate)
ClickData.Func.Command.RunCommand ("@Set TheDate = {$TheSelFileDate}")
TheFormattedDate = ClickData.Func.Command.Vars.get ("TheDate")


Two questions:

  1. This gives me the date in the form dd/MM/YY HH:mm:ss (which I could then manipulate using string methods). I don't understand why this happened, and why TheFormattedDate is not in the universal date form that it was previously.

  2. More importantly, it's all rather clumsy. Is there a direct JScript method of doing this, ideally something like:
    TheSelFileDate.format (YYMMdd)
    I could write such a method that would take into account all the various formatting possibilities, but it would be a very long function, and it's completely standard, so it must already have been done.

The JScript Date object lets you get its independent parts (day, month, year) so you can grab those and format them how you like - see below for an example. I don't know if there's a "format date" function or not.

var today = new Date();
var twoDigitYear = today.getYear();
twoDigitYear -= (twoDigitYear > 1999) ? 2000 : 1900;
DOpus.Output(zeroFill(twoDigitYear,2) + zeroFill(today.getMonth(), 2) + zeroFill(today.getDate(), 2));

function zeroFill( number, width )
{
  width -= number.toString().length;
  if ( width > 0 )
  {
    return new Array( width + (/\./.test( number ) ? 2 : 1) ).join( '0' ) + number;
  }
  return number + ""; // always return a string
}

Thanks, Jon — starting from methods such as getYear is more elegant. But it seems that essentially you do have to perform all the formatting yourself by manipulating strings and using the zerofill function. And if you want to display the name of a day or a month, you have to teach JScript the days of the week and the months of the year.

It may be worth composing a corresponding set of methods for the date object, creating such things as:
TheDate.TwoDigitDay, TheDate.DayName, . . . , TheDate.YYMMdd, . . .
(although googling for such standard things would probably be quicker).


This all brings up another "initial question" that I afraid I can't find in the manual:

  • Is it possible to construct a library of functions that any DOpus script can call?
  • If so, is /dopusdata\Scripts the best place for it?
  • and how do you call such a function from within a JScript?

jon, I believe that there is an error in the Online Manual at:
Scripting Reference - - > Scripting Objects - - > Item - - > Modify - - > Return Type
The "date modified" that is returned seems to be a string object, rather than a date object as stated in the "Return Type" column.

In my script, the command TheSelFileDateRaw = TheEnumFiles.item ().modify outputs (via DOpus.Output)
04/08/15 12:53:54
and does not allow methods such as TheSelFileDateRaw.getDate (). If, however, I change the command to
TheSelFileDateRaw = new Date (TheEnumFiles.item ().modify)
the output is the usual universal date, and the usual date object methods are allowed.

I expect that it's the same for all the dates on this page, although I haven't checked it. Perhaps some code is intended because the word "date" is not in hypertext, but neither are "string", "bool", "int", so at the very least, it's confusing.

For versatile date formatting, I tend to use this: gist.github.com/eralston/968809

Regarding function libraries, look out for "windows script components" or WSCs, there's already one or two threads about this topic, providing some links worth reading.

Opus returns variant dates (VT_DATE) which are language agnostic; if you print it will convert implicitly to a string but it's actually a date type, and if you create a JScript Date object from it it will convert it correctly.

Thanks, jon and tbone. I think I've now got all the information and references that I need, although it's going to take me some time to digest it all, particularly the organisation of libraries, which is far more complicated than I had expected.

Steven Levithan's date code is impressively condensed for all the things that it achieves.

I decided in the end to produce my own masks for the date — well, they are actually values in an associative array, but they look very similar to masks. The resulting function "DoMasks" works for all Gregorian dates (and therefore not before 1600 when the Julian calendar applied), and include some further useful masks, including:

  • ArrMask.dayabs is the "absolute" day nunber, with 01/01/2001 being Day 1. This function allows differences between dates to be calculated very easily based on the date alone, without worrying about milliseconds, daylight saving, rounding and so forth. Because this first day of the millennium was a Monday, ArrMask.dayabs modulo 7 happily coincides with JScript's "getDay ()".
  • ArrMask.dayy returns the day of the year, with 1st January being Day 1.
  • ArrMask.ord is the ordinal dates 1st, 2nd, 3rd, 4th, . . . that I was fussing about a little while ago.
  • ArrMask.leap is Boolean for leap year, and ArrMask.daysinmonth returns the days in the current month.
  • ArrMask.tt returns the traditional "noon" for midday instead of "pm". That is taking control!
  • At the bottom of the function are some standard formats of the whole date and/or time.
    This single array solves all the problems that I was worrying about, but I understand that it is an idiosyncratic solution. Other masks could easily be added, in particular, UTC codes.

I've left in place the first block, which allows easy testing of the various masks.

At the bottom is a proper integer division algorithm "DoDivision", again using an associative array, to replace the faulty modulo operator in JScript. JScript doesn't treat remainders properly when the dividend is negative — non-zero remainders are conventionally positive in mathematics, but JScript uses negative remainders. For example, JScript claims that
-11 = -4 (mod 7), instead of -11 = 3 (mod 7). In one sense, they are both correct, in that
-11 = 7 * (-1) - 4 and -11 = 7 * (-2) + 3, but only the second is standard, because swapping around confuses the structure of the ideal of all integers that are 3 modulo 7. This is quite serious with dates, because for example JScript reckons that 3 and -11 are unequal modulo 7, whereas they are the same day of the week! In the array produced by the function "DoDivision":

  • ArrDivision.r is the remainder, given as a non-negative whole number less than the divisor.
  • ArrDivision.q is the corresponding integer quotient. JScript omits the quotient, and again, comparing quotients is meaningless when remainders have been calculated differently for positive and negative integers.
    To demonstrate its usefulness, I've also included a further function "DoEuclid" that performs the Euclidean algorithm on two integers.

The "DoMasks" function can be used by copying it into any JScript, but it uses the three functions "DoDivision", "DoOrdinalEnding" and "DoBuffer", which need to be copied as well. The function "DoDate" creates a date object from two of the standard formats in "DoMasks" — it stands on its own, and allows checking by reversing the processes.

[code]@Script JScript
// Julianon 17/08/15 Six functions for dates and integer division.

// The following 30 lines are only for testing and may be deleted.
DOpus.Output ("- - - - - - - - >")

ObjDateX = DoDate ("160321@153409.456789")
DOpus.Output (DoMasks (ObjDateX).verbose + ", " + DoMasks (ObjDateX).standardss + "." + DoMasks (ObjDateX).fff)
DOpus.Output ("Compare DoMasks(ObjDateX).dayabs (modulo 7) = " + DoMasks (ObjDateX).dayabs + " (modulo 7) = " + DoDivision (DoMasks (ObjDateX).dayabs, 7).r + ", and ObjDateX.getDay() = " + ObjDateX.getDay ())
DOpus.Output ("- - - - - - - -")

ObjDateY = new Date
DOpus.Output (DoMasks (ObjDateY).verbose + ", " + DoMasks (ObjDateY).hhmmss + ". This is the " + DoMasks (ObjDateY).dayy + DoOrdinalEnding (DoMasks (ObjDateY).dayy) + " day of the year.")
DOpus.Output ("Compare DoMasks(ObjDateY).dayabs (modulo 7) = " + DoMasks (ObjDateY).dayabs + " (modulo 7) = " + DoDivision (DoMasks (ObjDateY).dayabs, 7).r + ", and ObjDateY.getDay() = " + ObjDateY.getDay ())
var TheTemp = DoMasks (ObjDateX).dayabs - DoMasks (ObjDateY).dayabs
DOpus.Output ("There are " + TheTemp + " days between this date and the previous date.")
DOpus.Output ("- - - - - - - -")

ObjDateZ = DoDate ("29/02/1800 12:00")
DOpus.Output (DoMasks (ObjDateZ).ddMMyyyy + ", " + DoMasks (ObjDateZ).hhmm)
DOpus.Output ("Compare DoMasks(ObjDateZ).dayabs (modulo 7) = " + DoMasks (ObjDateZ).dayabs + " (modulo 7) = " + DoDivision (DoMasks (ObjDateZ).dayabs, 7).r + ", and ObjDateZ.getDay() = " + ObjDateZ.getDay ())
DOpus.Output ("- - - - - - - -")

DOpus.Output ("-11 modulo 7 using JScript's modulo function: -11 % 7 = " + -11 % 7)
Arr7 = DoDivision (-11, 7)
DOpus.Output ("-11 divided by 7 using the DoDivision function below: " + Arr7.n + " divided by " + Arr7.d + " = " + Arr7.q + " remainder " + Arr7.r)
DOpus.Output ("- - - - - - - -")

Euclid = DoEuclid (21, -175)
DOpus.Output ("HCF(" + Euclid.a + ", " + Euclid.b + ") = " + Euclid.HCF + " and LCM(" + Euclid.a + ", " + Euclid.b + ") = " + Euclid.LCM)
TheTemp = Euclid.a * Euclid.x + Euclid.b * Euclid.y
DOpus.Output (Euclid.a + " times " + Euclid.x + " + " + Euclid.b + " times " + Euclid.y + " = " + Euclid.a * Euclid.x + " + " + Euclid.b * Euclid.y + " = " + TheTemp)

DOpus.Output ("< - - - - - - - -")

// FUNCTION DoMasks (ObjDate) THIS REQUIRES DoDivision (TheDividend, TheDivisor) AND DoOrdinalEnding (TheNumber) AND DoBuffer (TheNumber, TheLength).
function DoMasks (ObjDate) { // ObjDate must be a date object.
ArrMonthNames = new Array ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December")
ArrDayNames = new Array ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
ArrDaysInMonths = new Array (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) // The February value will be changed if it is a leap year.
var nn = 0

ArrMask = new Array // An associative array that will carry all the various masks, and is returned.

ArrMask.yyyy = ObjDate.getFullYear () // STEP 1: Input the data from ObjDate object:
ArrMask.M = ObjDate.getMonth () + 1
ArrMask.d = ObjDate.getDate ()
ArrMask.day = ObjDate.getDay ()
ArrMask.H = ObjDate.getHours ()
ArrMask.m = ObjDate.getMinutes ()
ArrMask.s = ObjDate.getSeconds ()
ArrMask.ms =ObjDate.getMilliseconds ()

ArrMask.yy = DoBuffer (ArrMask.yyyy, 2) // STEP 2: The two-digit masks and milliseconds.
ArrMask.y = 1 * ArrMask.yy
ArrMask.MM = DoBuffer (ArrMask.M, 2)
ArrMask.dd = DoBuffer (ArrMask.d, 2)
ArrMask.HH = DoBuffer (ArrMask.H, 2)
ArrMask.mm = DoBuffer (ArrMask.m, 2)
ArrMask.ss = DoBuffer (ArrMask.s, 2)
ArrMask.fff = DoBuffer (ArrMask.ms, 3)

ArrMask.h = ArrMask.H // STEP 3: 12 hour time with AM and PM and NOON.
ArrMask.hh = DoBuffer (ArrMask.h, 2)
ArrMask.tt = "am"
if (ArrMask.H > 12)
ArrMask.h = ArrMask.h - 12
if (ArrMask.H > 11)
ArrMask.tt = "pm"
if (ArrMask.H == 12 && ArrMask.m == 0 && ArrMask.s == 0 && ArrMask.ms == 0)
ArrMask.tt = "noon"

ArrMask.dddd = ArrDayNames [ArrMask.day] // STEP 4: The names and abbreviations of the month and the day.
ArrMask.ddd = String (ArrMask.dddd).substr (0, 3)
ArrMask.MMMM = ArrMonthNames [ArrMask.M - 1]
ArrMask.MMM = String (ArrMask.MMMM).substr (0, 3)

ArrMask.ord = ArrMask.d + DoOrdinalEnding (ArrMask.d) // STEP 5: The ordinal dates 1st, 2nd, 3rd, 4th, . . .

ArrMask.leap = false // STEP 6: A Boolean value for the leap year.
if (ArrMask.yyyy % 4 == 0 && (!(ArrMask.yyyy % 100 == 0) || ArrMask.yyyy % 400 == 0)) {// Is it a leap year?
ArrMask.leap = true
ArrDaysInMonths [1] = 29 // Change February to 29 days in leap years.
}

ArrMask.daysinmonth = ArrDaysInMonths [ArrMask.M-1] // STEP 7: The number of days in the date's month.

ArrMask.dayy = ArrMask.d // STEP 8: ArrMask.dayy will be the day of the year, with 01/01 being yday 1.
for (nn = 1; nn < ArrMask.M; nn++)
ArrMask.dayy = ArrMask.dayy + ArrDaysInMonths [nn - 1] // The leap year correction has already been made.

// STEP 9: ArrMask.dayabs will be the second millenium day absolute, with 01/01/2001 being day 1, and earlier days being negative.
ArrEuclid400 = DoDivision (ArrMask.yyyy, 400)
ArrMask.dayabs = 146097 * (ArrEuclid400.q - 5) // There are 146097 day in each Gregorian 400-year period
ArrMask.dayabs = ArrMask.dayabs + (365 * (ArrEuclid400.r - 1)) // Add 365 days for each completed year.
for (nn = 0; nn < ArrEuclid400.r; nn = nn + 4) {
if (!(nn % 400 == 100 || nn % 400 == 200 || nn % 400 == 300))
ArrMask.dayabs = ArrMask.dayabs + 1 // Add 1 more day for each completed leap year after 2000.
}
ArrMask.dayabs = ArrMask.dayabs + ArrMask.dayy // Add the day-of-the-year function.
ArrMask.dayabs = ArrMask.dayabs - 1 // Subtract 1 to make 01/01/2001 day 1.

// STEP 10: Some common date formats.
ArrMask.ddMMyy = ArrMask.dd + "/" + ArrMask.MM + "/" + ArrMask.yy // With dashes.
ArrMask.ddMMyyyy = ArrMask.dd + "/" + ArrMask.MM + "/" + ArrMask.yyyy // With dashes.
ArrMask.yyMMdd = ArrMask.yy + ArrMask.MM + ArrMask.dd // Without dashes.
ArrMask.HHmm = ArrMask.HH + ":" + ArrMask.mm // With a colon.
ArrMask.HHmmss = ArrMask.HH + ":" + ArrMask.mm + ":" + ArrMask.ss // With colons.
ArrMask.hhmm = ArrMask.hh + ":" + ArrMask.mm + " " + ArrMask.tt // With a colon.
ArrMask.hhmmss = ArrMask.hh + ":" + ArrMask.mm + ":" + ArrMask.ss + " " + ArrMask.tt // With colons.
ArrMask.standard = ArrMask.ddMMyy + " " + ArrMask.HHmm // For listers.
ArrMask.standardss = ArrMask.ddMMyy + " " + ArrMask.HHmmss // Add the seconds.
ArrMask.reverse = ArrMask.yyMMdd + "@" + ArrMask.HH + ArrMask.mm + ArrMask.ss // For use in filenames.
ArrMask.verbose = ArrMask.dddd + ", " + ArrMask.ord + " " + ArrMask.MMMM + " " + ArrMask.yyyy // Ceremonial.

return ArrMask
}

// FUNCTION DoDivision (TheDividend, TheDivisor)*************************************************************
function DoDivision (TheDividend, TheDivisor) { // Intended only for integers, with TheDivisor nonzero.
ArrDivision = new Array // This is an associative array that will carry the four elements, and be returned.

ArrDivision.n = TheDividend // This is the dividend
ArrDivision.d = TheDivisor // This is the divisor
ArrDivision.r = TheDividend % TheDivisor
if (ArrDivision.r < 0) // The remainder should be a positive number.
ArrDivision.r = ArrDivision.r + Math.abs (TheDivisor) // This is the remainder
ArrDivision.q = Math.round ((TheDividend - ArrDivision.r) / TheDivisor) // This is the quotient.
return ArrDivision
}

// FUNCTION DoOrdinalEnding (TheNumber)***********************************************************************
function DoOrdinalEnding (TheNumber) { // Intended only for non-negative whole numbers.
StrOrdEnding = new String ("th") // This will be the ordinal ending, and will be returned.
var TheMod10 = TheNumber % 10
var TheMod100 = TheNumber % 100

if (TheMod10 == 1 && !(TheMod100 == 11))
StrOrdEnding = "st"
if (TheMod10 == 2 && !(TheMod100 == 12))
StrOrdEnding = "nd"
if (TheMod10 == 3 && !(TheMod100 == 13))
StrOrdEnding = "rd"
return StrOrdEnding
}

// FUNCTION DoBuffer (TheNumber, TheLength)******************************************************************
function DoBuffer (TheNumber, TheLength) { // Intended only for two non-negative whole numbers.
StrNumber = new String ("") // This will be the buffered or truncated number.

StrNumber = TheNumber.toString ()
while (StrNumber.length < TheLength) // This bit adds zeroes at the front if the string is too short.
StrNumber = "0" + StrNumber
if (StrNumber.length > TheLength) // This bit truncates from the left if the string is too long.
StrNumber = StrNumber.substr (StrNumber.length - TheLength)
return StrNumber
}

// FUNCTION DoDate (TheDate)*********************************************************************************
// TheDate may in one of two forms: Either "27/8/2034 1:23:45.678" or "340827@012345.678"
// where if any time element is missing, all subsequent time elements are missing.
function DoDate (TheDate) {
var nn = 0
var TheTemp = new String ("")
ArrFull = TheDate.split (" ")
ArrDate = ArrFull [0].split ("/")

// FIRST FORM: d/M/yyyy H:m:s.fff where d, M, H, m, s may be dd, MM, HH, mm, ss respectively, but .fff is a decimal.
if (ArrDate.length == 3) {
TheTemp = ArrDate [0]
ArrDate [0] = ArrDate [2]
ArrDate [2] = TheTemp
for (nn = 3; nn < 7; nn++)
ArrDate [nn] = 0
if (!(ArrFull [1] == undefined)) {
ArrTime = ArrFull [1].split (":")
ArrDate [3] = ArrTime [0]
if (ArrTime.length > 1)
ArrDate [4] = ArrTime [1]
if (ArrTime.length > 2) {
ArrDate [5] = Math.floor (ArrTime [2])
ArrDate [6] = Math.round (1000 * (ArrTime [2] - ArrDate [5]))
}
}
} else {

// SECOND FORM: yyMMdd@mmHHss.fff where each is 2-buffered, and 20yy is assumed, except that .fff is a decimal.
ArrFull = TheDate.split ("@")
ArrDate = new Array
ArrDate [0] = ArrFull [0].substr (0, 2)
ArrDate [1] = ArrFull [0].substr (2, 2)
ArrDate [2] = ArrFull [0].substr (4, 2)
ArrDate [3] = ArrFull [1].substr (0, 2)
ArrDate [4] = ArrFull [1].substr (2, 2)
ArrDate [5] = ArrFull [1].substr (4, 2)
ArrDate [6] = Math. round (1000 * ArrFull [1].substr (6))
}

if ((ArrDate [0]).toString ().length == 2) {
ArrDate [0] = "20" + ArrDate [0]
DOpus.Output ("WARNING: The date " + TheDate + " was interpreted as being in the year " + ArrDate [0] + ".")
}
ObjDate = new Date (ArrDate [0], ArrDate [1] - 1, ArrDate [2], ArrDate [3], ArrDate [4], ArrDate [5], ArrDate [6])
return ObjDate
}

// FUNCTION DoEuclid (a, b) THIS REQUIRES THE FUNCTION DoDivision (TheDividend, TheDivisor)******************
function DoEuclid (a, b) {
Euclid = new Array // This associative array will carry a and b, the HCF and LCM, and x and y so ax + by = HCF.
var nn = 0
var TheTemp = 0
var TheOrder = 0
a = Math.round (a) // Should be unnecessary.
b = Math.round (b)
if (Math.abs (a) > Math.abs (b)) { // Make sure that |a| <= |b|.
TheTemp = a
a = b
b = TheTemp
}
q = new Array // These are the successive quotients.
r = new Array (a, b) // r [2], r [3], ... are the successive remainders.
x = new Array // x and y are for tracking backwards. Eventually a * x[0] + b * y[0] = HCF.
y = new Array
Euclid.a = a
Euclid.b = b

// STEP 1: First deal with either or both integers zero, and with two integers equal in absolute value.
if (a == 0 || Math.abs (a) == Math.abs (b)) {
Euclid.HCF = Math.abs (b)
Euclid.LCM = Math.abs (a)
Euclid.x = 0
Euclid.y = 0
if (b > 0)
Euclid.y = 1
if (b < 0)
Euclid.y = -1
} else {

// STEP 2: Otherwise, carry out the Euclidean algorithm and record the successive quotients and remainders.
TheTemp = 0
nn = 0
while (!(r [nn + 1] == 0) && nn < 500) { // The condition nn < 500 is to protect from an infinite loop.
q [nn] = DoDivision (r [nn], r [nn + 1]).q
r [nn + 2] = DoDivision (r [nn], r [nn + 1]).r
DOpus.Output (r [nn] + " divided by " + r [nn + 1] + " = " + q [nn] + " remainder " + r [nn + 2]) // Delete after testing.
nn++
}
TheOrder = nn
Euclid.HCF = Math.abs (r [TheOrder])
Euclid.LCM = Math.abs (Math.round ((Euclid.a * Euclid.b) / Euclid.HCF))

// STEP 3: Then track backwards through the division steps to find integers x and y such that ax + by = HCF.
x [TheOrder - 2] = 1
y [TheOrder - 2] = - q [TheOrder - 2]
for (nn = TheOrder - 2; nn > 0; nn = nn - 1) {
x [nn - 1] = y [nn]
y [nn - 1] = x [nn] - (q [nn - 1] * y [nn])
}
Euclid.x = x [0]
Euclid.y = y [0]
} // Closes "if" in Step 1.
return Euclid
}[/code]
I would also like to refer to two excellent online JScript references that between them cleared up many difficulties I had (only readable after jon and tbone's advice and encouragement):
ns7.webmasters.com/caspdoc/html/ ... erence.htm
tergestesoft.com/~manuals/jslang/jstoc.htm

Two more questions about JScript related to the constructions above:

  1. Should I have been using constructors rather than associative arrays for the functions "DoMasks", DoDivision" and "DoEuclid" functions? The two can be changed one into the other with only a few very minor alterations to the code.

More than that, is there any actual difference in JScript between an associative array and a constructor? It seems that the word "this" can be changed to "anything" throughout the function, including the return, provided only that there is an initial declaration var anything = new Array .

  1. I have been using arrays and associative arrays rather than vectors and maps, because arrays and associative arrays will work anywhere, not just inside DOpus. I only ran into a problem when I tried to save an array as a DOpus variable.

Does DOpus have future plans to accommodate arrays and associative arrays into all aspects of DOpus' version of JScript?

JScript arrays are not automation-compatible so Opus has no way of seeing them or working with them. This is actually the reason we added our own Vector object in the first place.