How to Format Dates, Numbers, and Translated Text Using apex.date and apex.locale

A tested guide to apex.date, apex.locale, and apex.lang in Oracle APEX, from Oracle format masks and date math to translated text messages.

JavaScript's own Date and Number formatting knows nothing about Oracle format masks, the session's NLS settings, or the text messages you translate in Shared Components. Oracle APEX fills that gap with three namespaces. apex.lang brings the application's translatable text messages into JavaScript. apex.locale knows the session's language and separators and formats numbers the way TO_CHAR does. apex.date adds, compares, formats, and parses dates with the same masks you use in SQL.

This guide covers all three with tested examples and real output, flags the behaviors that catch people out in APEX 26.1, and ends with two recipes: checking a date range as the user types, and defaulting a date to a number of working days later.

Quick Reference

TaskFunction
Load text messages on demandapex.lang.loadMessages, loadMessagesIfNeeded
Read a text messageapex.lang.getMessage, hasMessage
Fill in message parametersapex.lang.formatMessage, formatMessageNoEscape, format, formatNoEscape
Add your own messagesapex.lang.addMessages
Read the session's language, separators, and date formatsapex.locale getters
Format and parse numbersapex.locale.formatNumber, formatCompactNumber, toNumber
Add and subtract timeapex.date.add, subtract, clone
Format and parse dates with Oracle masksapex.date.format, parse, toISOString
Compare datesapex.date.isBefore, isAfter, isSame, isBetween, min, max
Month, week, and day helpersfirstOfMonth, lastOfMonth, ISOWeek, startOfDay, dayOfWeek, and more
Show "3 hours ago"apex.date.since

How to Run These Examples

The examples ran in Oracle APEX 26.1 in an English session with the application date format DS, on a machine set to India Standard Time. The output under each example is exactly what the browser console printed.

Most examples work on any page: press F12, paste the code into the Console tab, and run it. The apex.lang examples use text messages defined in the test application, such as ORBIT_DISCOUNT_APPROVAL; create messages of your own under Shared Components, Text Messages, and use their names instead. Several examples use await at the top level, which the console allows; inside a dynamic action, wrap such code in an async function.

Translatable Text with apex.lang

Translatable text in JavaScript comes from the application's text messages, which are translated along with the rest of the application. Messages with Used in JavaScript switched on are loaded with every page; the others can be loaded when needed. For how translation works overall, see the guide to globalization and translation in Oracle APEX.

apex.lang.loadMessages

Loads text messages from the server and returns a promise. A key ending in % loads every message whose name starts with it.

apex.lang.loadMessages(pMessageKeys) → Promise
console.log("loaded before:", apex.lang.hasMessage("ORBIT_DISCOUNT_APPROVAL"));
await apex.lang.loadMessages(["ORBIT_DISCOUNT_APPROVAL", "ORDER_P10ORDERNUMBER_HAS_BEEN_%"]);
console.log("loaded after: ", apex.lang.hasMessage("ORBIT_DISCOUNT_APPROVAL"));
console.log(apex.lang.getMessage("ORDER_P10ORDERNUMBER_HAS_BEEN_CANCELLED"));
console.log(apex.lang.getMessage("NO_SUCH_MESSAGE"));

Output:

loaded before: false
loaded after:  true
Order %P10_ORDER_NUMBER has been cancelled.
NO_SUCH_MESSAGE

Note the last line. getMessage returns the key itself for a message it does not have, so a missing message shows up on the page as its name rather than as an error. If you see a raw key like NO_SUCH_MESSAGE in your UI, the message either does not exist or was never loaded.

apex.lang.loadMessagesIfNeeded

Calls the callback once the messages are available: immediately if they are already loaded, otherwise after a request to the server.

apex.lang.loadMessagesIfNeeded(pMessageKeys, pCallback)
apex.lang.loadMessagesIfNeeded(["ORBIT_DISCOUNT_APPROVAL"], () => {
    console.log("first call:", apex.lang.formatMessage("ORBIT_DISCOUNT_APPROVAL", 15));
    apex.lang.loadMessagesIfNeeded(["ORBIT_DISCOUNT_APPROVAL"], () => console.log("second call: no request"));
});

Output:

first call: A discount above 15% needs approval. Set the status to Pending Approval.
second call: no request

This is the one to use in code that may run many times, such as a click handler, because only the first call costs a request.

apex.lang.getMessage and hasMessage

getMessage returns the text of a loaded message, or the key when it is not loaded. hasMessage tells you whether a message is loaded. The loadMessages example above and the addMessages example below both use them.

apex.lang.getMessage(pKey) → string
apex.lang.hasMessage(pKey) → boolean

apex.lang.formatMessage and formatMessageNoEscape

Return a message with its parameters filled in. Parameters are either positional, %0 to %9, or named, such as %P10_ORDER_NUMBER, filled from an object. formatMessage escapes the values for HTML; formatMessageNoEscape leaves them as they are, for text going into textContent or values you know are safe.

apex.lang.formatMessage(pKey, ...pValues) → string
apex.lang.formatMessageNoEscape(pKey, ...pValues) → string
await apex.lang.loadMessages(["ORBIT_DISCOUNT_APPROVAL", "ORDER_P10ORDERNUMBER_HAS_BEEN_SUBMITTED"]);
console.log(apex.lang.formatMessage("ORBIT_DISCOUNT_APPROVAL", 15));                        // %0
console.log(apex.lang.formatMessage("ORDER_P10ORDERNUMBER_HAS_BEEN_SUBMITTED",
                                    { P10_ORDER_NUMBER: "<b>ORD-12283</b>" }));            // named
console.log(apex.lang.formatMessageNoEscape("ORDER_P10ORDERNUMBER_HAS_BEEN_SUBMITTED",
                                            { P10_ORDER_NUMBER: "<b>ORD-12283</b>" }));

Output:

A discount above 15% needs approval. Set the status to Pending Approval.
Order &lt;b&gt;ORD-12283&lt;&#x2F;b&gt; has been submitted.
Order <b>ORD-12283</b> has been submitted.

The escaped version is the safe default for anything that ends up in HTML. Reach for the NoEscape variant only when the destination is plain text or the values are under your control.

apex.lang.format and formatNoEscape

The same idea, but with the pattern itself instead of a message key, for text that is not translated or has already been. %% stands for a literal percent sign.

apex.lang.format(pPattern, ...pValues) → string
apex.lang.formatNoEscape(pPattern, ...pValues) → string
console.log(apex.lang.format("%0 of %1 orders are %2.", 3, 12, "<b>late</b>"));
console.log(apex.lang.formatNoEscape("%0 of %1 orders are %2.", 3, 12, "<b>late</b>"));
console.log(apex.lang.format("Hello %name, you have %count new tasks.", { name: "Ada", count: 2 }));
console.log(apex.lang.format("100%% of %0", "orders"));

Output:

3 of 12 orders are &lt;b&gt;late&lt;&#x2F;b&gt;.
3 of 12 orders are <b>late</b>.
Hello Ada, you have 2 new tasks.
100% of orders

apex.lang.addMessages and clearMessages

addMessages adds messages of your own, from a plug-in for example, to the ones already loaded. clearMessages removes all messages, including the ones APEX's own components need, so you rarely want it.

apex.lang.addMessages(pMessages)
apex.lang.clearMessages()
apex.lang.addMessages({
    "ORBIT.CART.ITEMS": "%0 items in your cart",
    "ORBIT.CART.EMPTY": "Your cart is empty"
});
console.log(apex.lang.formatMessage("ORBIT.CART.ITEMS", 3));
console.log(apex.lang.hasMessage("ORBIT.CART.EMPTY"), apex.lang.getMessage("ORBIT.CART.EMPTY"));
apex.lang.clearMessages();
console.log("after clearMessages:", apex.lang.hasMessage("ORBIT.CART.EMPTY"),
            apex.lang.hasMessage("APEX.DIALOG.OK"));

Output:

3 items in your cart
true Your cart is empty
after clearMessages: false false

The last line shows why clearMessages is dangerous: APEX.DIALOG.OK, one of APEX's own messages, is gone too, and built-in dialogs would show raw keys until the page reloads.

Session Locale with apex.locale

A page's locale is the language and NLS settings of the session, taken from the application's Globalization attributes or from the language the user chose.

The Locale Getters

Return the settings of the session's locale. The date formats are the application's Application Date Format and the database's DS and DL formats. Day and month names come in the session language, starting from the territory's first day of the week.

apex.locale.getLanguage() → string
apex.locale.getDecimalSeparator() → string
apex.locale.getGroupSeparator() → string
apex.locale.getCurrency() → string
apex.locale.getISOCurrency() → string
apex.locale.getDualCurrency() → string
apex.locale.getDateFormat() → string
apex.locale.getDSDateFormat() → string
apex.locale.getDLDateFormat() → string
apex.locale.getDayNames() → string[]
apex.locale.getAbbrevDayNames() → string[]
apex.locale.getMonthNames() → string[]
apex.locale.getAbbrevMonthNames() → string[]
const l = apex.locale;
console.log("language:", l.getLanguage());
console.log("decimal:", l.getDecimalSeparator(), "- group:", l.getGroupSeparator());
console.log("currency:", l.getCurrency(), l.getISOCurrency(), l.getDualCurrency());
console.log("date formats:", l.getDateFormat(), "|", l.getDSDateFormat(), "|", l.getDLDateFormat());
console.log("days:", l.getDayNames().join(" "));
console.log("abbrev:", l.getAbbrevDayNames().join(" "), "|", l.getAbbrevMonthNames().join(" "));
console.log("months:", l.getMonthNames().slice(0, 3).join(" "), "…");

Output:

language: en
decimal: . - group: ,
currency: $ USD $
date formats: DS | fmMM/DD/RRRR | fmDay, Month fmdd, yyyy
days: Sunday Monday Tuesday Wednesday Thursday Friday Saturday
abbrev: Sun Mon Tue Wed Thu Fri Sat | Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
months: January February March …

The application date format here is DS, and apex.date.format resolves it to the DS mask shown next to it. Use these getters instead of hard-coding a comma or a dollar sign, and the same code works for a German or Indian user without changes.

apex.locale.formatNumber

Formats a number with an Oracle number format model, like TO_CHAR, using the locale's separators and currency. The format elements RN, TM, and EEEE are not supported. Without a format, it only localizes the decimal point.

apex.locale.formatNumber(pValue, [pFormat], [pOptions]) → string
OptionTypeDescription
NLS_NUMERIC_CHARACTERSstringThe decimal and group separators, such as ",.".
NLS_CURRENCY, NLS_DUAL_CURRENCYstringThe currency symbols used by L and U.
NLS_ISO_CURRENCYstringThe ISO currency used by C, such as "EUR".
const f = apex.locale.formatNumber;
console.log(f(1234567.891));
console.log(f(1234567.891, "FML999G999G990D00"));
console.log(f(-42.5, "999G990D00MI"));
console.log(f(1234.5, "L99G999D99PR"), f(-1234.5, "L99G999D99PR"));
console.log(f(1234567.891, "999G999G990D00", { NLS_NUMERIC_CHARACTERS: ",." }));
console.log(f(9.95, "C990D00", { NLS_ISO_CURRENCY: "EUR" }));

Output:

1234567.891
$1,234,567.89
     42.50-
           $1,234.50            <$1,234.50>
   1.234.567,89
       EUR9.95

Just like TO_CHAR, the result is padded to the width of the format, with leading blanks and a trailing blank or sign for MI and PR, unless the format starts with FM. That padding is why several lines above start with spaces. Start the mask with FM whenever the result goes into a sentence or a narrow cell. For a simpler take on number formatting, see formatting numbers with commas and decimals using JavaScript.

apex.locale.formatCompactNumber

Formats a number in a short form, such as 123.4K or 1.23M. It needs locale resources that load with the page, so code that runs at page load should wait for resourcesLoaded first.

apex.locale.formatCompactNumber(pValue, [pOptions]) → string
OptionTypeDescription
maximumFractionDigits, minimumFractionDigits, minimumIntegerDigitsnumberDigit limits. The defaults are 2, 0, and 1.
roundingModestringDEFAULT, HALF_UP, HALF_DOWN, HALF_EVEN, UP, DOWN, CEILING, or FLOOR.
useGroupingbooleanUse group separators. The default is true.
separatorsobject{decimal, group} to use instead of the locale's.
await apex.locale.resourcesLoaded();
for (const n of [950, 123400, 1234000, 2500000000]) {
    console.log(n, "->", apex.locale.formatCompactNumber(n), "|",
                apex.locale.formatCompactNumber(n, { maximumFractionDigits: 0 }));
}

Output:

950 -> 950 | 950
123400 -> 123.4K | 123K
1234000 -> 1.23M | 1M
2500000000 -> 2.5B | 3B

Compact numbers suit dashboard tiles and badges where space is tight. With maximumFractionDigits set to 0, 2.5 billion rounds up to 3B, so keep at least one fraction digit where the difference matters.

apex.locale.toNumber

Turns a formatted string into a number, stripping currency symbols, group separators, and other format characters. It is deliberately lenient so it accepts what users type, and returns null when the text is not a number.

apex.locale.toNumber(pValue, [pFormat]) → number | null
const t = apex.locale.toNumber;
console.log(t("1,234,567.89"));
console.log(t("$1,234.50", "FML999G990D00"));
console.log(t("42-", "990MI"));
console.log(t("abc"));

Output:

1234567.89
1234.5
-42
null

Use it before doing arithmetic on the value of a number item with a format mask. Adding "1,234.50" directly in JavaScript gives you string concatenation or NaN, not a sum.

apex.locale.resourcesLoaded

Returns a promise, and calls an optional callback, once the resources that formatCompactNumber and date formatting in other languages depend on have loaded.

apex.locale.resourcesLoaded([pCallback]) → Promise
apex.locale.resourcesLoaded(() => console.log("callback: resources loaded"));
const result = await apex.locale.resourcesLoaded();
console.log("promise resolved:", result);

Output:

callback: resources loaded
promise resolved: undefined

Dates with apex.date

apex.date calculates with JavaScript Date objects and formats and parses them with Oracle format masks. Most functions take the date as their first argument and fall back to the current date and time without it.

Two rules prevent most bugs. First, add, subtract, and setDayOfYear change the Date object you pass them, so clone a date before changing it if you still need the original. Second, units are given with the constants of apex.date.UNIT: MILLISECOND (the default), SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, and YEAR.

apex.date.add, subtract, and clone

add and subtract change a date by an amount of a unit and return it. clone returns a copy.

apex.date.add([pDate], pAmount, [pUnit]) → Date
apex.date.subtract([pDate], pAmount, [pUnit]) → Date
apex.date.clone(pDate) → Date
const { UNIT } = apex.date;
const ordered = new Date(2026, 8, 23, 14, 30);            // 23 September 2026, 14:30
const due = apex.date.add(apex.date.clone(ordered), 3, UNIT.DAY);
console.log("ordered:", apex.date.format(ordered, "DD-MON-YYYY HH24:MI"));
console.log("due:    ", apex.date.format(due, "DD-MON-YYYY HH24:MI"));
apex.date.subtract(due, 2, UNIT.HOUR);
console.log("minus 2 hours:", apex.date.format(due, "DD-MON-YYYY HH24:MI"));
apex.date.add(ordered, 1, UNIT.MONTH);                    // without clone: changes the original
console.log("ordered is now:", apex.date.format(ordered, "DD-MON-YYYY"));
console.log(UNIT);

Output:

ordered: 23-SEP-2026 14:30
due:     26-SEP-2026 14:30
minus 2 hours: 26-SEP-2026 12:30
ordered is now: 23-OCT-2026
{
  "MILLISECOND": "millisecond",
  "SECOND": "second",
  "MINUTE": "minute",
  "HOUR": "hour",
  "DAY": "day",
  "WEEK": "week",
  "MONTH": "month",
  "YEAR": "year"
}

The fourth line shows the trap: adding a month to ordered without cloning it first changed the order date itself. Also note that JavaScript months start at 0, so new Date(2026, 8, 23) is 23 September.

apex.date.format

Formats a date with an Oracle date format mask, the application's date format by default. Masks containing SYEAR, SYYYY, IYYY, YEAR, IYY, SCC, or TZ are not supported.

apex.date.format([pDate], [pFormat], [pLocale]) → string
const d = new Date(2026, 8, 23, 14, 30, 5);
console.log(apex.date.format(d));                          // the application's date format
console.log(apex.date.format(d, "Day, DD Month YYYY"));
console.log(apex.date.format(d, "fmDay, DD fmMonth YYYY \"at\" HH:MI AM"));
console.log(apex.date.format(d, "YYYY-MM-DD\"T\"HH24:MI:SS"));
console.log(apex.date.format(d, "DD.MM.YYYY", "de"));
try {
    apex.date.format(d, "DD. Month YYYY", "de");
} catch (e) {
    console.log("de, Month:", e.message);
}

Output:

9/23/2026
Wednesday, 23 September 2026
Wednesday, 23 September 2026 at 02:30 PM
2026-09-23T14:30:05
23.09.2026
de, Month: Only english & current application language are supported for 'MONTH' format

Two differences from TO_CHAR are worth knowing. Day and month names are available only in English and the session language, so with another pLocale a mask with Day, Month, or Mon throws, as the last line shows; numeric masks work in any locale. And Day and Month are not padded with blanks as they are in SQL, so fm makes no difference to them.

apex.date.parse

Turns a string into a date using an Oracle format mask, the application's date format by default. It throws a Date Parsing Error for a string that does not match the mask or is not a valid date, so always wrap it in try and catch.

apex.date.parse(pDateString, [pFormat]) → Date
const iso = (d) => apex.date.toISOString(d);
console.log(iso(apex.date.parse("23-SEP-2026 14:30", "DD-MON-YYYY HH24:MI")));
console.log(iso(apex.date.parse("9/23/2026")));                 // the application's date format
try {
    apex.date.parse("31-FEB-2026", "DD-MON-YYYY");
} catch (e) {
    console.log("31-FEB-2026:", e.message);
}
console.log(apex.date.isValid(new Date("x")), apex.date.isValid(new Date(2026, 8, 23)));
console.log(apex.date.isValidString("2026-09-23T14:30"), apex.date.isValidString("next Tuesday"));

Output:

2026-09-23T14:30:00
2026-09-23T00:00:00
31-FEB-2026: Date Parsing Error
false true
true false

The example also uses two helpers. isValid checks that an object is a valid Date, and isValidString checks whether the browser's Date.parse can read a string, which in practice means ISO 8601.

Comparing Dates: isBefore, isAfter, isSame, isSameOrBefore, isSameOrAfter, isBetween, min, and max

Compare dates. The optional unit sets the precision: with UNIT.DAY, two times on the same day count as the same. isBetween excludes both bounds. min and max return the earliest and latest of any number of dates.

apex.date.isBefore(pDate1, pDate2, [pUnit]) → boolean
apex.date.isBetween(pDate1, pDate2, pDate3, [pUnit]) → boolean
apex.date.min(...pDates) → Date
const { UNIT } = apex.date;
const a = new Date(2026, 8, 23, 9, 0), b = new Date(2026, 8, 23, 17, 45), c = new Date(2026, 9, 1);
console.log("isBefore:", apex.date.isBefore(a, b), "- same day:", apex.date.isBefore(a, b, UNIT.DAY));
console.log("isAfter:", apex.date.isAfter(c, b, UNIT.MONTH));
console.log("isSame:", apex.date.isSame(a, b), apex.date.isSame(a, b, UNIT.DAY));
console.log("isSameOrBefore:", apex.date.isSameOrBefore(a, b, UNIT.DAY),
            "- isSameOrAfter:", apex.date.isSameOrAfter(a, c, UNIT.YEAR));
console.log("isBetween:", apex.date.isBetween(b, a, c), apex.date.isBetween(a, a, c));
console.log("min:", apex.date.format(apex.date.min(b, c, a), "DD-MON HH24:MI"),
            "- max:", apex.date.format(apex.date.max(b, c, a), "DD-MON HH24:MI"));

Output:

isBefore: true - same day: false
isAfter: true
isSame: false true
isSameOrBefore: true - isSameOrAfter: true
isBetween: true false
min: 23-SEP 09:00 - max: 01-OCT 00:00

9:00 is before 17:45 on the same day, but not at day precision, where the two are the same. Comparing with UNIT.DAY is almost always what you want for date items that carry no meaningful time.

Month and Week Functions

firstOfMonth and lastOfMonth return new dates. daysInMonth, weekOfMonth, ISOWeek (the ISO 8601 week of the year), isLeapYear, and monthsBetween return numbers and booleans.

apex.date.firstOfMonth([pDate]) → Date
apex.date.lastOfMonth([pDate]) → Date
apex.date.daysInMonth([pDate]) → number
apex.date.weekOfMonth([pDate]) → number
apex.date.ISOWeek([pDate]) → number
apex.date.isLeapYear([pDate]) → boolean
apex.date.monthsBetween(pDate1, pDate2) → number
const d = new Date(2026, 1, 17);                           // 17 February 2026
const f = (x) => apex.date.format(x, "DD-MON-YYYY");
console.log("firstOfMonth:", f(apex.date.firstOfMonth(d)), "- lastOfMonth:", f(apex.date.lastOfMonth(d)));
console.log("daysInMonth:", apex.date.daysInMonth(d), "- leap year:", apex.date.isLeapYear(d),
            apex.date.isLeapYear(new Date(2028, 0, 1)));
console.log("weekOfMonth:", apex.date.weekOfMonth(d), "- ISOWeek:", apex.date.ISOWeek(d));
console.log("monthsBetween:", apex.date.monthsBetween(new Date(2026, 8, 23), d));

Output:

firstOfMonth: 01-FEB-2026 - lastOfMonth: 28-FEB-2026
daysInMonth: 28 - leap year: false true
weekOfMonth: 3 - ISOWeek: 8
monthsBetween: 7

Unlike the SQL function MONTHS_BETWEEN, monthsBetween counts whole months and is always positive, whichever date comes first.

Day Functions

startOfDay and endOfDay return new dates at 00:00:00 and 23:59:59.999. dayOfWeek (0 is Sunday), getDayOfYear, and secondsPastMidnight return numbers. setDayOfYear moves a date to a given day of its year.

apex.date.startOfDay([pDate]) → Date
apex.date.endOfDay([pDate]) → Date
apex.date.dayOfWeek([pDate]) → number
apex.date.getDayOfYear([pDate]) → number
apex.date.secondsPastMidnight([pDate]) → number
apex.date.setDayOfYear([pDate], pDay)
const d = new Date(2026, 8, 23, 14, 30, 5);
const f = (x) => apex.date.format(x, "DD-MON-YYYY HH24:MI:SS");
console.log("startOfDay:", f(apex.date.startOfDay(d)));
console.log("endOfDay:  ", f(apex.date.endOfDay(d)));
console.log("dayOfWeek:", apex.date.dayOfWeek(d), "- getDayOfYear:", apex.date.getDayOfYear(d),
            "- secondsPastMidnight:", apex.date.secondsPastMidnight(d));
const first = apex.date.clone(d);
console.log("setDayOfYear returns:", apex.date.setDayOfYear(first, 1));
console.log("the date is now:", f(first));

Output:

startOfDay: 23-SEP-2026 00:00:00
endOfDay:   23-SEP-2026 23:59:59
dayOfWeek: 3 - getDayOfYear: 266 - secondsPastMidnight: 52205
setDayOfYear returns: undefined
the date is now: 01-JAN-2026 14:30:05

The documentation says setDayOfYear returns the date, but in 26.1 it returns undefined and only changes the date you pass it, as the output shows. Keep the date in a variable and read that variable afterwards.

apex.date.since

Returns how long ago a date was, in words, the JavaScript counterpart of APEX_UTIL.GET_SINCE, or in a short form when pShort is true. Future dates work too.

apex.date.since([pDate], [pShort]) → string
const { UNIT } = apex.date;
const ago = (amount, unit) => apex.date.subtract(new Date(), amount, unit);
for (const [amount, unit] of [[30, UNIT.SECOND], [5, UNIT.MINUTE], [3, UNIT.HOUR], [2, UNIT.DAY], [7, UNIT.MONTH]]) {
    console.log(`${amount} ${unit}:`.padEnd(14), apex.date.since(ago(amount, unit)), "|", apex.date.since(ago(amount, unit), true));
}
console.log("in 2 days:".padEnd(14), apex.date.since(apex.date.add(new Date(), 2, UNIT.DAY)));

Output:

30 second:     30 seconds ago | 30s
5 minute:      5 minutes ago | 5m
3 hour:        3 hours ago | 3h
2 day:         2 days ago | 2d
7 month:       7 months ago | 7mo
in 2 days:     2 days from now

apex.date.toISOString

Returns the date as an ISO 8601 string in local time, with no time zone. That is the format of date values in APEX JSON and the one apex.date.parse reads. The Date object's own toISOString converts to UTC instead.

apex.date.toISOString([pDate]) → string
const d = new Date(2026, 8, 23, 14, 30, 5);
console.log(apex.date.toISOString(d), "(apex.date: local time, no time zone)");
console.log(d.toISOString(), "(Date: UTC, here from India Standard Time)");

Output:

2026-09-23T14:30:05 (apex.date: local time, no time zone)
2026-09-23T09:00:05.000Z (Date: UTC, here from India Standard Time)

On a machine five and a half hours ahead of UTC, the built-in method moves 14:30 back to 09:00 and adds a Z. Sending that to the server is a classic source of dates that are off by one day, so use apex.date.toISOString when the value represents local time.

Recipes

Check a Date Range as the User Types

An order's Required By date must not be before the order date, nor more than 60 days after it. This check parses both items with their format mask, compares them at day precision with apex.date.isBefore and isAfter, and shows or clears an inline error while the user is still on the page, instead of after a submit.

Put the function in the page's Function and Global Variable Declaration attribute, and call it from a dynamic action on the Change event of both date items. To block the submit as well, call it from an apexbeforepagesubmit handler and cancel the submit when it returns false, as described in the guide to the apex namespace and page events. The code at the top only opens an order so there is something to check.

// === Try it: open an order from the Orders page ===
$("#orders a[href*='order']").first()[0].click();
// --- on the Order page ---
// === Page › Function and Global Variable Declaration ===
// Required By must be on or after the order date, and no more than 60 days later.
function checkRequiredDate() {
    const { UNIT } = apex.date;
    const mask = "DD-MON-YYYY";
    let ordered, required;
    try {
        ordered  = apex.date.parse(apex.item("P10_ORDER_DATE").getValue(), mask);
        required = apex.date.parse(apex.item("P10_REQUIRED_DATE").getValue(), mask);
    } catch (e) {
        return true;                          // empty or not a date: Value Required handles that
    }
    let problem = null;
    if (apex.date.isBefore(required, ordered, UNIT.DAY)) {
        problem = "Required By cannot be before the order date.";
    } else if (apex.date.isAfter(required, apex.date.add(apex.date.clone(ordered), 60, UNIT.DAY),
                                 UNIT.DAY)) {
        problem = "Required By must be within 60 days of the order date.";
    }
    apex.message.clearErrors();
    if (problem) {
        apex.message.showErrors({ type: "error", location: "inline", pageItem: "P10_REQUIRED_DATE",
                                  message: problem, unsafe: false });
    }
    return !problem;
}
// === Dynamic action on Change of P10_ORDER_DATE, P10_REQUIRED_DATE › Execute JavaScript Code ===
checkRequiredDate();
// === Try it: three dates for an order of 23-SEP-2026 ===
for (const date of ["20-SEP-2026", "30-DEC-2026", "15-OCT-2026"]) {
    apex.item("P10_REQUIRED_DATE").setValue(date);
    console.log(date, "→", checkRequiredDate() ? "valid" : $("#P10_REQUIRED_DATE_error").text());
}
apex.item("P10_REQUIRED_DATE").setValue("30-DEC-2026");      // (for the picture)
checkRequiredDate();

Output:

20-SEP-2026 → Required By cannot be before the order date.
30-DEC-2026 → Required By must be within 60 days of the order date.
15-OCT-2026 → valid
An Oracle APEX Required By date item showing an inline error about the 60-day limit
The inline error appears under Required By as soon as the date is out of range.

apex.date.parse throws for a value that is not a date, so the function catches it and leaves empty and invalid values to the item's own Value Required and date checks. Pass the item's format mask, here the application's DD-MON-YYYY, as the second argument. The error display itself is covered in the guide to showing messages with apex.message.

Default a Date to a Number of Working Days Later

A new order should be required five working days after it was placed. apex.date.add moves the date forward one day at a time, and the loop counts only days that are not Saturday or Sunday; apex.date.format then writes the result in the item's format.

Put the function in the page's Function and Global Variable Declaration attribute, and call it from a dynamic action on the Change event of P10_ORDER_DATE.

// === Try it: open an order from the Orders page ===
$("#orders a[href*='order']").first()[0].click();
// --- on the Order page ---
// === Page › Function and Global Variable Declaration ===
// Adds working days to a date: Saturdays and Sundays do not count.
function addBusinessDays(date, days) {
    const result = apex.date.clone(date);
    while (days > 0) {
        apex.date.add(result, 1, apex.date.UNIT.DAY);
        if (result.getDay() !== 0 && result.getDay() !== 6) days--;
    }
    return result;
}
// === Dynamic action on Change of P10_ORDER_DATE › Execute JavaScript Code ===
const ordered = apex.date.parse(apex.item("P10_ORDER_DATE").getValue(), "DD-MON-YYYY");
apex.item("P10_REQUIRED_DATE").setValue(apex.date.format(addBusinessDays(ordered, 5), "DD-MON-YYYY"));
// === Try it: the order date and the new Required By date ===
console.log("ordered:", apex.item("P10_ORDER_DATE").getValue(),
            apex.date.format(ordered, "Day").trim());
console.log("required by:", apex.item("P10_REQUIRED_DATE").getValue(),
            apex.date.format(addBusinessDays(ordered, 5), "Day").trim());

Output:

ordered: 23-SEP-2026 Wednesday
required by: 30-SEP-2026 Wednesday

apex.date.clone keeps the order date unchanged, since add changes the date it is given. To skip public holidays too, pass their dates in and skip any day for which apex.date.isSame with UNIT.DAY matches a holiday. Setting item values from code is covered in the guide to getting and setting page items with apex.item.

Conclusion

apex.lang loads translatable text messages and fills in positional or named parameters, escaping the values unless you choose a NoEscape variant, and returns the key itself when a message is missing. apex.locale reports the session's language, separators, and date formats, formats numbers with Oracle format models, padding included unless you use FM, and parses what users type with toNumber. apex.date adds, compares, formats, and parses dates with Oracle masks. Keep its three traps in mind: parse throws on bad input, add and setDayOfYear change the date you pass them, and setDayOfYear returns nothing in 26.1. Use apex.date.toISOString rather than the built-in method when a date means local time.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE and software veteran with 25+ years of experience, passionate about AI and IT innovation.

guest

0 Comments
Oldest
Newest Most Voted
00