Skip to content

Commit 7f741fe

Browse files
authored
fix(date-range): cast dates to YYYY-MM-DD when passing to python executor (#167)
1 parent 333a169 commit 7f741fe

File tree

2 files changed

+207
-13
lines changed

2 files changed

+207
-13
lines changed

src/notebooks/deepnote/converters/inputConverters.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ import {
1818
import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants';
1919
import { formatInputBlockCellContent } from '../inputBlockContentFormatter';
2020

21+
/** Converts date strings to YYYY-MM-DD format, preserving values already in that format. */
22+
function normalizeDateString(dateValue: unknown): string {
23+
if (!dateValue || typeof dateValue !== 'string') {
24+
return '';
25+
}
26+
27+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateValue)) {
28+
return dateValue;
29+
}
30+
31+
// Detect ISO-style strings that start with YYYY-MM-DD (e.g., "2025-09-30T00:00:00+02:00")
32+
// and extract just the date portion to avoid timezone shifts
33+
if (/^\d{4}-\d{2}-\d{2}/.test(dateValue)) {
34+
return dateValue.substring(0, 10);
35+
}
36+
37+
try {
38+
const date = new Date(dateValue);
39+
if (isNaN(date.getTime())) {
40+
return dateValue;
41+
}
42+
const year = date.getUTCFullYear();
43+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
44+
const day = String(date.getUTCDate()).padStart(2, '0');
45+
return `${year}-${month}-${day}`;
46+
} catch {
47+
return dateValue;
48+
}
49+
}
50+
2151
export abstract class BaseInputBlockConverter<T extends z.ZodObject> implements BlockConverter {
2252
abstract schema(): T;
2353
abstract getSupportedType(): string;
@@ -248,8 +278,20 @@ export class InputDateBlockConverter extends BaseInputBlockConverter<typeof Deep
248278
return cell;
249279
}
250280

251-
// Date blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
252-
// Uses base class applyChangesToBlock which preserves existing metadata
281+
/**
282+
* Normalizes ISO date strings to YYYY-MM-DD format expected by createPythonCode.
283+
* Deepnote API may return dates like "2025-09-30T00:00:00.000Z".
284+
*/
285+
override applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
286+
const value = block.metadata?.deepnote_variable_value;
287+
288+
if (typeof value === 'string' && value) {
289+
const normalizedValue = normalizeDateString(value);
290+
this.updateBlockMetadata(block, { deepnote_variable_value: normalizedValue });
291+
} else {
292+
this.updateBlockMetadata(block, {});
293+
}
294+
}
253295
}
254296

255297
export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof DeepnoteDateRangeInputMetadataSchema> {
@@ -271,8 +313,21 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof
271313
return cell;
272314
}
273315

274-
// Date range blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
275-
// Uses base class applyChangesToBlock which preserves existing metadata
316+
/**
317+
* Normalizes ISO date strings to YYYY-MM-DD format expected by createPythonCode.
318+
* Deepnote API may return dates like ["2025-09-30T00:00:00.000Z", "2025-10-16T00:00:00.000Z"].
319+
* Relative date strings like "past3months" are preserved as-is.
320+
*/
321+
override applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
322+
const value = block.metadata?.deepnote_variable_value;
323+
324+
if (Array.isArray(value) && value.length === 2) {
325+
const normalizedValue: [string, string] = [normalizeDateString(value[0]), normalizeDateString(value[1])];
326+
this.updateBlockMetadata(block, { deepnote_variable_value: normalizedValue });
327+
} else {
328+
this.updateBlockMetadata(block, {});
329+
}
330+
}
276331
}
277332

278333
export class InputFileBlockConverter extends BaseInputBlockConverter<typeof DeepnoteFileInputMetadataSchema> {

src/notebooks/deepnote/converters/inputConverters.unit.test.ts

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ suite('InputDateBlockConverter', () => {
700700
});
701701

702702
suite('applyChangesToBlock', () => {
703-
test('preserves existing metadata (date blocks are readonly)', () => {
703+
test('normalizes ISO date string to YYYY-MM-DD', () => {
704704
const block: DeepnoteBlock = {
705705
blockGroup: 'test-group',
706706
content: '',
@@ -713,17 +713,76 @@ suite('InputDateBlockConverter', () => {
713713
sortingKey: 'a0',
714714
type: 'input-date'
715715
};
716-
// Cell content is ignored since date blocks are readonly
717-
const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-12-31T00:00:00.000Z"', 'python');
716+
const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-01-01"', 'python');
718717

719718
converter.applyChangesToBlock(block, cell);
720719

721720
assert.strictEqual(block.content, '');
722-
// Value should be preserved from metadata, not parsed from cell
723-
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-01-01T00:00:00.000Z');
721+
// ISO date should be normalized to YYYY-MM-DD
722+
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-01-01');
724723
assert.strictEqual(block.metadata?.deepnote_variable_name, 'date1');
725724
assert.strictEqual(block.metadata?.deepnote_input_date_version, 2);
726725
});
726+
727+
test('preserves dates already in YYYY-MM-DD format', () => {
728+
const block: DeepnoteBlock = {
729+
blockGroup: 'test-group',
730+
content: '',
731+
id: 'block-123',
732+
metadata: {
733+
deepnote_variable_name: 'date1',
734+
deepnote_input_date_version: 2,
735+
deepnote_variable_value: '2025-06-15'
736+
},
737+
sortingKey: 'a0',
738+
type: 'input-date'
739+
};
740+
const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-06-15"', 'python');
741+
742+
converter.applyChangesToBlock(block, cell);
743+
744+
// YYYY-MM-DD format should remain unchanged
745+
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-06-15');
746+
});
747+
748+
test('handles timezone-aware ISO dates correctly', () => {
749+
const block: DeepnoteBlock = {
750+
blockGroup: 'test-group',
751+
content: '',
752+
id: 'block-123',
753+
metadata: {
754+
deepnote_variable_name: 'date1',
755+
deepnote_variable_value: '2025-09-30T00:00:00.000Z'
756+
},
757+
sortingKey: 'a0',
758+
type: 'input-date'
759+
};
760+
const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-09-30"', 'python');
761+
762+
converter.applyChangesToBlock(block, cell);
763+
764+
// Should extract the date portion from ISO string
765+
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-09-30');
766+
});
767+
768+
test('preserves empty or null values', () => {
769+
const block: DeepnoteBlock = {
770+
blockGroup: 'test-group',
771+
content: '',
772+
id: 'block-123',
773+
metadata: {
774+
deepnote_variable_name: 'date1',
775+
deepnote_variable_value: ''
776+
},
777+
sortingKey: 'a0',
778+
type: 'input-date'
779+
};
780+
const cell = new NotebookCellData(NotebookCellKind.Code, '""', 'python');
781+
782+
converter.applyChangesToBlock(block, cell);
783+
784+
assert.strictEqual(block.metadata?.deepnote_variable_value, '');
785+
});
727786
});
728787
});
729788

@@ -791,7 +850,29 @@ suite('InputDateRangeBlockConverter', () => {
791850
});
792851

793852
suite('applyChangesToBlock', () => {
794-
test('preserves existing metadata (date range blocks are readonly)', () => {
853+
test('normalizes ISO date range to YYYY-MM-DD format', () => {
854+
const block: DeepnoteBlock = {
855+
blockGroup: 'test-group',
856+
content: '',
857+
id: 'block-123',
858+
metadata: {
859+
deepnote_variable_name: 'range1',
860+
deepnote_variable_value: ['2025-09-30T00:00:00.000Z', '2025-10-16T00:00:00.000Z']
861+
},
862+
sortingKey: 'a0',
863+
type: 'input-date-range'
864+
};
865+
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-09-30", "2025-10-16")', 'python');
866+
867+
converter.applyChangesToBlock(block, cell);
868+
869+
assert.strictEqual(block.content, '');
870+
// ISO dates should be normalized to YYYY-MM-DD
871+
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-09-30', '2025-10-16']);
872+
assert.strictEqual(block.metadata?.deepnote_variable_name, 'range1');
873+
});
874+
875+
test('preserves date ranges already in YYYY-MM-DD format', () => {
795876
const block: DeepnoteBlock = {
796877
blockGroup: 'test-group',
797878
content: '',
@@ -803,16 +884,74 @@ suite('InputDateRangeBlockConverter', () => {
803884
sortingKey: 'a0',
804885
type: 'input-date-range'
805886
};
806-
// Cell content is ignored since date range blocks are readonly
807-
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-01-01", "2025-12-31")', 'python');
887+
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-06-01", "2025-06-30")', 'python');
808888

809889
converter.applyChangesToBlock(block, cell);
810890

811891
assert.strictEqual(block.content, '');
812-
// Value should be preserved from metadata, not parsed from cell
892+
// YYYY-MM-DD format should remain unchanged
813893
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-06-01', '2025-06-30']);
814894
assert.strictEqual(block.metadata?.deepnote_variable_name, 'range1');
815895
});
896+
897+
test('normalizes mixed ISO and YYYY-MM-DD dates', () => {
898+
const block: DeepnoteBlock = {
899+
blockGroup: 'test-group',
900+
content: '',
901+
id: 'block-123',
902+
metadata: {
903+
deepnote_variable_name: 'range1',
904+
deepnote_variable_value: ['2025-01-01T00:00:00.000Z', '2025-12-31']
905+
},
906+
sortingKey: 'a0',
907+
type: 'input-date-range'
908+
};
909+
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-01-01", "2025-12-31")', 'python');
910+
911+
converter.applyChangesToBlock(block, cell);
912+
913+
// Both should be normalized to YYYY-MM-DD
914+
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']);
915+
});
916+
917+
test('preserves relative date strings', () => {
918+
const block: DeepnoteBlock = {
919+
blockGroup: 'test-group',
920+
content: '',
921+
id: 'block-123',
922+
metadata: {
923+
deepnote_variable_name: 'range1',
924+
deepnote_variable_value: 'past3months'
925+
},
926+
sortingKey: 'a0',
927+
type: 'input-date-range'
928+
};
929+
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python');
930+
931+
converter.applyChangesToBlock(block, cell);
932+
933+
// Relative date strings should be preserved
934+
assert.strictEqual(block.metadata?.deepnote_variable_value, 'past3months');
935+
});
936+
937+
test('handles empty date range values', () => {
938+
const block: DeepnoteBlock = {
939+
blockGroup: 'test-group',
940+
content: '',
941+
id: 'block-123',
942+
metadata: {
943+
deepnote_variable_name: 'range1',
944+
deepnote_variable_value: ['', '']
945+
},
946+
sortingKey: 'a0',
947+
type: 'input-date-range'
948+
};
949+
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python');
950+
951+
converter.applyChangesToBlock(block, cell);
952+
953+
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['', '']);
954+
});
816955
});
817956
});
818957

0 commit comments

Comments
 (0)