Skip to content

Commit 83b5d23

Browse files
authored
Merge pull request #2353 from teableio/fix/T1466-formula-timezone
fix(T1466): default formula timeZone and add NOW formula test
2 parents beef847 + 5446d7a commit 83b5d23

File tree

7 files changed

+103
-9
lines changed

7 files changed

+103
-9
lines changed

apps/nestjs-backend/src/features/field/field-calculate/field-supplement.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,13 +772,17 @@ export class FieldSupplementService {
772772

773773
const formatting =
774774
(fieldRo.options as IFormulaFieldOptions)?.formatting ?? getDefaultFormatting(cellValueType);
775+
const timeZone =
776+
(fieldRo.options as IFormulaFieldOptions)?.timeZone ??
777+
Intl.DateTimeFormat().resolvedOptions().timeZone;
775778

776779
return {
777780
...fieldRo,
778781
name: fieldRo.name ?? 'Calculation',
779782
options: {
780783
...fieldRo.options,
781784
...(formatting ? { formatting } : {}),
785+
timeZone,
782786
},
783787
cellValueType,
784788
isMultipleCellValue,

apps/nestjs-backend/src/features/record/query-builder/field-select-visitor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export class FieldSelectVisitor implements IFieldVisitor<IFieldSelectName> {
285285
}
286286

287287
const expression = field.getExpression();
288-
const timezone = field.options.timeZone;
288+
const timezone = field.options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
289289

290290
// In raw/propagation context (used by UPDATE ... FROM SELECT), avoid referencing
291291
// the physical generated column directly, since it may have been dropped by

apps/nestjs-backend/test/field-converting.e2e-spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ describe('OpenAPI Freely perform column transformations (e2e)', () => {
238238
const { newField } = await expectUpdate(table1, sourceFieldRo, newFieldRo);
239239
expect(newField.options).toEqual({
240240
expression: '"text"',
241+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
241242
});
242243
});
243244

apps/nestjs-backend/test/field.e2e-spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,14 @@ describe('OpenAPI FieldController (e2e)', () => {
202202
});
203203

204204
it('formula field', async () => {
205+
const defaultTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
205206
const stringFormulaField = await createFieldByType(FieldType.Formula, {
206207
expression: '"A"',
207208
});
208209
expect(stringFormulaField.name).toEqual('Calculation');
209210
expect(stringFormulaField.options).toEqual({
210211
expression: '"A"',
212+
timeZone: defaultTimeZone,
211213
});
212214

213215
const numberFormulaField = await createFieldByType(FieldType.Formula, {
@@ -216,13 +218,15 @@ describe('OpenAPI FieldController (e2e)', () => {
216218
expect(numberFormulaField.options).toEqual({
217219
expression: '1 + 1',
218220
formatting: { type: NumberFormattingType.Decimal, precision: 2 },
221+
timeZone: defaultTimeZone,
219222
});
220223

221224
const booleanFormulaField = await createFieldByType(FieldType.Formula, {
222225
expression: 'true',
223226
});
224227
expect(booleanFormulaField.options).toEqual({
225228
expression: 'true',
229+
timeZone: defaultTimeZone,
226230
});
227231

228232
const datetimeField = await createFieldByType(FieldType.Date);
@@ -234,8 +238,9 @@ describe('OpenAPI FieldController (e2e)', () => {
234238
formatting: {
235239
date: DateFormattingPreset.ISO,
236240
time: TimeFormatting.None,
237-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
241+
timeZone: defaultTimeZone,
238242
},
243+
timeZone: defaultTimeZone,
239244
});
240245
});
241246

apps/nestjs-backend/test/formula.e2e-spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6496,6 +6496,85 @@ describe('OpenAPI formula (e2e)', () => {
64966496
expect(record2.data.fields[field2.name]).toEqual(27);
64976497
});
64986498

6499+
it('should default formula timeZone when missing', async () => {
6500+
const inputIso = '2024-02-28T00:00:00+09:00';
6501+
const defaultTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
6502+
6503+
const field = await createField(table.id, {
6504+
type: FieldType.Formula,
6505+
options: {
6506+
expression: `DAY("${inputIso}")`,
6507+
},
6508+
});
6509+
6510+
const fieldOptions = field.options as { timeZone?: string } | undefined;
6511+
expect(fieldOptions?.timeZone).toEqual(defaultTimeZone);
6512+
6513+
const record = await getRecord(table.id, table.records[0].id);
6514+
const expectedDay = Number(
6515+
new Intl.DateTimeFormat('en-GB', {
6516+
timeZone: defaultTimeZone,
6517+
day: '2-digit',
6518+
}).format(new Date(inputIso))
6519+
);
6520+
6521+
expect(record.data.fields[field.name]).toEqual(expectedDay);
6522+
});
6523+
6524+
it('should bucket Created On records using NOW() formula', async () => {
6525+
const createdOnField = await createField(table.id, {
6526+
name: 'Created On',
6527+
type: FieldType.Date,
6528+
options: {
6529+
formatting: {
6530+
date: DateFormattingPreset.ISO,
6531+
time: TimeFormatting.Hour24,
6532+
timeZone: 'UTC',
6533+
},
6534+
},
6535+
});
6536+
6537+
const formulaField = await createField(table.id, {
6538+
name: 'Pitch Day',
6539+
type: FieldType.Formula,
6540+
options: {
6541+
expression: `IF(DATETIME_DIFF(NOW(), {${createdOnField.id}}, "day")<1, "Today", IF(DATETIME_DIFF(NOW(), {${createdOnField.id}}, "day")<2, "Yesterday", "Older"))`,
6542+
timeZone: 'UTC',
6543+
},
6544+
});
6545+
6546+
const now = Date.now();
6547+
const records = await createRecords(table.id, {
6548+
fieldKeyType: FieldKeyType.Id,
6549+
records: [
6550+
{
6551+
fields: {
6552+
[createdOnField.id]: new Date(now - 2 * 60 * 60 * 1000).toISOString(),
6553+
},
6554+
},
6555+
{
6556+
fields: {
6557+
[createdOnField.id]: new Date(now - 26 * 60 * 60 * 1000).toISOString(),
6558+
},
6559+
},
6560+
{
6561+
fields: {
6562+
[createdOnField.id]: new Date(now - 3 * 24 * 60 * 60 * 1000).toISOString(),
6563+
},
6564+
},
6565+
],
6566+
});
6567+
6568+
const todayRecord = await getRecord(table.id, records.records[0].id);
6569+
expect(todayRecord.data.fields[formulaField.name]).toEqual('Today');
6570+
6571+
const yesterdayRecord = await getRecord(table.id, records.records[1].id);
6572+
expect(yesterdayRecord.data.fields[formulaField.name]).toEqual('Yesterday');
6573+
6574+
const olderRecord = await getRecord(table.id, records.records[2].id);
6575+
expect(olderRecord.data.fields[formulaField.name]).toEqual('Older');
6576+
});
6577+
64996578
it('should evaluate timezone-aware formatting formulas referencing fields', async () => {
65006579
const dateField = await createField(table.id, {
65016580
name: 'tz source',

packages/core/src/formula/functions/date-time.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ describe('DateTime', () => {
3636
const todayFunc = new Today();
3737

3838
it('should return the current date', () => {
39-
const result = todayFunc.eval();
39+
const timeZone = 'America/Los_Angeles';
40+
const result = todayFunc.eval([], {
41+
record: {} as IRecord,
42+
dependencies: {},
43+
timeZone,
44+
});
4045

41-
expect(result).toBe(dayjs().startOf('d').toISOString());
46+
expect(result).toBe(dayjs().tz(timeZone).startOf('d').toISOString());
4247
});
4348
});
4449

packages/core/src/formula/functions/date-time.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,13 @@ export class Today extends DateTimeFunc {
9191
return { type: CellValueType.DateTime };
9292
}
9393

94-
eval(): string | null {
95-
return dayjs().startOf('d').toISOString();
94+
eval(_params: TypedValue[], context: IFormulaContext): string | null {
95+
return dayjs().tz(context.timeZone).startOf('d').toISOString();
9696
}
9797
}
9898

9999
export class Now extends DateTimeFunc {
100-
name = FunctionName.Today;
100+
name = FunctionName.Now;
101101

102102
acceptValueType = new Set([]);
103103

@@ -111,8 +111,8 @@ export class Now extends DateTimeFunc {
111111
return { type: CellValueType.DateTime };
112112
}
113113

114-
eval(): string | null {
115-
return dayjs().toISOString();
114+
eval(_params: TypedValue[], context: IFormulaContext): string | null {
115+
return dayjs().tz(context.timeZone).toISOString();
116116
}
117117
}
118118

0 commit comments

Comments
 (0)