Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- uses: actions/setup-node@v2
with:
node-version: "20"
node-version: lts/*

- name: Install NPM
run: |
Expand Down Expand Up @@ -51,7 +51,7 @@ jobs:

- uses: actions/setup-node@v2
with:
node-version: "20"
node-version: lts/*

- name: Install NPM
run: |
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/pr_build.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
name: PR Build

# `pull_request`, not `pull_request_target`. The latter reads its workflow definition and its
# checkout from the base branch, so changes to this file only take effect after merging to main,
# and the job tests main rather than the pull request's code. Forks get no secrets and cannot run
# this, which is correct.
on:
pull_request_target:
pull_request:
branches: [ main ]
types: [ opened, reopened, synchronize, closed ]
types: [ opened, reopened, synchronize ]

concurrency:
group: ${{ github.ref }}
Expand All @@ -17,7 +21,7 @@ jobs:

- uses: actions/setup-node@v2
with:
node-version: "20"
node-version: "22.14.0"

- name: Install dependencies
run: npm install
Expand Down Expand Up @@ -52,4 +56,5 @@ jobs:
fi

- name: Clean up scratch org
if: always()
run: sf org scratch delete --target-org=scratch-org --no-prompt
4 changes: 2 additions & 2 deletions docs/src/app/docs/functions/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,9 @@ DATETIME(2020, 1, 1, 12, 0, 0) // 2020-01-01 12:00:00

### DATETIMEFORMAT

Formats a DateTime into a string using the provided format.
Formats a Date or DateTime into a string using the provided format.

Accepts 2 arguments: the DateTime to format and the format string.
Accepts 2 arguments: the Date or DateTime to format and the format string.

```
DATETIMEFORMAT(DATETIMEVALUE("2020-01-01 12:00:00"), "yyyy-MM-dd") // 2020-01-01
Expand Down
2 changes: 1 addition & 1 deletion expression-src/main/editor/lwc/functions/functions-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ export const data = [
{
"name": "DATETIMEFORMAT",
"autoCompleteValue": "DATETIMEFORMAT(",
"description": "Formats a DateTime into a string using the provided format.<br/><br/>Accepts 2 arguments: the DateTime to format and the format string.",
"description": "Formats a Date or DateTime into a string using the provided format.<br/><br/>Accepts 2 arguments: the Date or DateTime to format and the format string.",
"examples": [
"DATETIMEFORMAT(DATETIMEVALUE(\"2020-01-01 12:00:00\"), \"yyyy-MM-dd\") // 2020-01-01"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public with sharing class DateAndTimeFunctions {
MILLISECOND, MINUTE, SECOND, HOUR_FN
}

// A bare Date carries no time or timezone, so anchor it at GMT midnight. Callers must read it
// back in GMT (formatGmt/getTime) or the calendar day shifts for users west of GMT.
private static Datetime gmtMidnight(Date value) {
return Datetime.newInstanceGmt(value, Time.newInstance(0, 0, 0, 0));
}

/**
* @description Returns a date that is a specified number of days before or after a given date.
*
Expand Down Expand Up @@ -591,7 +597,7 @@ public with sharing class DateAndTimeFunctions {
);
}

return Integer.valueOf(Datetime.newInstanceGmt((Date)dateValue, Time.newInstance(0, 0, 0, 0)).format('Y'));
return Integer.valueOf(gmtMidnight((Date)dateValue).format('Y'));
}

public override Arity getArity() {
Expand Down Expand Up @@ -673,7 +679,7 @@ public with sharing class DateAndTimeFunctions {

if (dateOrDateTimeOrTime instanceof Date) {
// Convert to datetime first
Datetime gmtDatetime = Datetime.newInstanceGmt((Date)dateOrDateTimeOrTime, Time.newInstance(0, 0, 0, 0));
Datetime gmtDatetime = gmtMidnight((Date)dateOrDateTimeOrTime);
return gmtDatetime.getTime() / 1000;
} else if (dateOrDateTimeOrTime instanceof Datetime) {
Datetime gmtDateTime = Datetime.newInstanceGmt(((Datetime)dateOrDateTimeOrTime).date(), ((Datetime)dateOrDateTimeOrTime).time());
Expand Down Expand Up @@ -707,16 +713,7 @@ public with sharing class DateAndTimeFunctions {
);
}

Date dateObj = (Date)dateValue;

return Integer.valueOf(Datetime.newInstanceGmt(
dateObj.year(),
dateObj.month(),
dateObj.day(),
0,
0,
0
).format('u'));
return Integer.valueOf(gmtMidnight((Date)dateValue).format('u'));
}

public override Arity getArity() {
Expand Down Expand Up @@ -821,31 +818,37 @@ public with sharing class DateAndTimeFunctions {
}

/**
* @description Formats a DateTime into a string using the provided format.
* @description Formats a Date or DateTime into a string using the provided format.
*
* Accepts 2 arguments: the DateTime to format and the format string.
* Accepts 2 arguments: the Date or DateTime to format and the format string.
* @function DATETIMEFORMAT
* @example
* DATETIMEFORMAT(DATETIMEVALUE("2020-01-01 12:00:00"), "yyyy-MM-dd") // 2020-01-01
*/
private class DateTimeFormatFn extends StandardFunction {
public override Object call(List<Expr> arguments) {
// Expect a DateTime
// A Date also passes `instanceof Datetime` in Apex, so check for Date first.
Object dateTimeValue = evaluate(arguments.get(0));
if (!(dateTimeValue instanceof Datetime)) {
Boolean isDate = dateTimeValue instanceof Date;
if (!isDate && !(dateTimeValue instanceof Datetime)) {
throw new FunctionExecutionException(
'Error executing "DATETIMEFORMAT" function: the argument must evaluate to a datetime value.'
'Error executing "DATETIMEFORMAT" function: the first argument must evaluate to a date or datetime value.'
);
}

// Expect a format string
Object formatValue = evaluate(arguments.get(1));
if (!(formatValue instanceof String)) {
throw new FunctionExecutionException(
'Error executing "DATETIMEFORMAT" function: the argument must evaluate to a string value.'
'Error executing "DATETIMEFORMAT" function: the second argument must evaluate to a string value.'
);
}

if (isDate) {
// Format in GMT to match the anchor, so the calendar day is rendered as-is.
return gmtMidnight((Date)dateTimeValue).formatGmt((String)formatValue);
}

return ((Datetime)dateTimeValue).format((String)formatValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ private class DateAndTimeFunctionsTest {
Assert.areEqual('2015-01-01 01:00:00', result);
}

@IsTest
private static void dateTimeFormatFormatsADateWithoutShiftingTheDay() {
Assert.areEqual('2026-09-09', Evaluator.run('DATETIMEFORMAT(DATE(2026, 9, 9), "yyyy-MM-dd")'));
}

@IsTest
private static void dateToDateTimeConvertsADateToADateTime() {
String formula = 'DATETODATETIME(DATEVALUE("2015-01-01"))';
Expand Down
Loading