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
63 changes: 59 additions & 4 deletions src/notebooks/deepnote/converters/inputConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ import {
import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants';
import { formatInputBlockCellContent } from '../inputBlockContentFormatter';

/** Converts date strings to YYYY-MM-DD format, preserving values already in that format. */
function normalizeDateString(dateValue: unknown): string {
if (!dateValue || typeof dateValue !== 'string') {
return '';
}

if (/^\d{4}-\d{2}-\d{2}$/.test(dateValue)) {
return dateValue;
}

// Detect ISO-style strings that start with YYYY-MM-DD (e.g., "2025-09-30T00:00:00+02:00")
// and extract just the date portion to avoid timezone shifts
if (/^\d{4}-\d{2}-\d{2}/.test(dateValue)) {
return dateValue.substring(0, 10);
}

try {
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
return dateValue;
}
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
} catch {
return dateValue;
}
}

export abstract class BaseInputBlockConverter<T extends z.ZodObject> implements BlockConverter {
abstract schema(): T;
abstract getSupportedType(): string;
Expand Down Expand Up @@ -248,8 +278,20 @@ export class InputDateBlockConverter extends BaseInputBlockConverter<typeof Deep
return cell;
}

// Date blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
// Uses base class applyChangesToBlock which preserves existing metadata
/**
* Normalizes ISO date strings to YYYY-MM-DD format expected by createPythonCode.
* Deepnote API may return dates like "2025-09-30T00:00:00.000Z".
*/
override applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
const value = block.metadata?.deepnote_variable_value;

if (typeof value === 'string' && value) {
const normalizedValue = normalizeDateString(value);
this.updateBlockMetadata(block, { deepnote_variable_value: normalizedValue });
} else {
this.updateBlockMetadata(block, {});
}
}
}

export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof DeepnoteDateRangeInputMetadataSchema> {
Expand All @@ -271,8 +313,21 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof
return cell;
}

// Date range blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
// Uses base class applyChangesToBlock which preserves existing metadata
/**
* Normalizes ISO date strings to YYYY-MM-DD format expected by createPythonCode.
* Deepnote API may return dates like ["2025-09-30T00:00:00.000Z", "2025-10-16T00:00:00.000Z"].
* Relative date strings like "past3months" are preserved as-is.
*/
override applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
const value = block.metadata?.deepnote_variable_value;

if (Array.isArray(value) && value.length === 2) {
const normalizedValue: [string, string] = [normalizeDateString(value[0]), normalizeDateString(value[1])];
this.updateBlockMetadata(block, { deepnote_variable_value: normalizedValue });
} else {
this.updateBlockMetadata(block, {});
}
}
}

export class InputFileBlockConverter extends BaseInputBlockConverter<typeof DeepnoteFileInputMetadataSchema> {
Expand Down
157 changes: 148 additions & 9 deletions src/notebooks/deepnote/converters/inputConverters.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ suite('InputDateBlockConverter', () => {
});

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

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.content, '');
// Value should be preserved from metadata, not parsed from cell
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-01-01T00:00:00.000Z');
// ISO date should be normalized to YYYY-MM-DD
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-01-01');
assert.strictEqual(block.metadata?.deepnote_variable_name, 'date1');
assert.strictEqual(block.metadata?.deepnote_input_date_version, 2);
});

test('preserves dates already in YYYY-MM-DD format', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'date1',
deepnote_input_date_version: 2,
deepnote_variable_value: '2025-06-15'
},
sortingKey: 'a0',
type: 'input-date'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-06-15"', 'python');

converter.applyChangesToBlock(block, cell);

// YYYY-MM-DD format should remain unchanged
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-06-15');
});

test('handles timezone-aware ISO dates correctly', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'date1',
deepnote_variable_value: '2025-09-30T00:00:00.000Z'
},
sortingKey: 'a0',
type: 'input-date'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '"2025-09-30"', 'python');

converter.applyChangesToBlock(block, cell);

// Should extract the date portion from ISO string
assert.strictEqual(block.metadata?.deepnote_variable_value, '2025-09-30');
});

test('preserves empty or null values', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'date1',
deepnote_variable_value: ''
},
sortingKey: 'a0',
type: 'input-date'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '""', 'python');

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.metadata?.deepnote_variable_value, '');
});
});
});

Expand Down Expand Up @@ -791,7 +850,29 @@ suite('InputDateRangeBlockConverter', () => {
});

suite('applyChangesToBlock', () => {
test('preserves existing metadata (date range blocks are readonly)', () => {
test('normalizes ISO date range to YYYY-MM-DD format', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'range1',
deepnote_variable_value: ['2025-09-30T00:00:00.000Z', '2025-10-16T00:00:00.000Z']
},
sortingKey: 'a0',
type: 'input-date-range'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-09-30", "2025-10-16")', 'python');

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.content, '');
// ISO dates should be normalized to YYYY-MM-DD
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-09-30', '2025-10-16']);
assert.strictEqual(block.metadata?.deepnote_variable_name, 'range1');
});

test('preserves date ranges already in YYYY-MM-DD format', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
Expand All @@ -803,16 +884,74 @@ suite('InputDateRangeBlockConverter', () => {
sortingKey: 'a0',
type: 'input-date-range'
};
// Cell content is ignored since date range blocks are readonly
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-01-01", "2025-12-31")', 'python');
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-06-01", "2025-06-30")', 'python');

converter.applyChangesToBlock(block, cell);

assert.strictEqual(block.content, '');
// Value should be preserved from metadata, not parsed from cell
// YYYY-MM-DD format should remain unchanged
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-06-01', '2025-06-30']);
assert.strictEqual(block.metadata?.deepnote_variable_name, 'range1');
});

test('normalizes mixed ISO and YYYY-MM-DD dates', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'range1',
deepnote_variable_value: ['2025-01-01T00:00:00.000Z', '2025-12-31']
},
sortingKey: 'a0',
type: 'input-date-range'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '("2025-01-01", "2025-12-31")', 'python');

converter.applyChangesToBlock(block, cell);

// Both should be normalized to YYYY-MM-DD
assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['2025-01-01', '2025-12-31']);
});

test('preserves relative date strings', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'range1',
deepnote_variable_value: 'past3months'
},
sortingKey: 'a0',
type: 'input-date-range'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python');

converter.applyChangesToBlock(block, cell);

// Relative date strings should be preserved
assert.strictEqual(block.metadata?.deepnote_variable_value, 'past3months');
});

test('handles empty date range values', () => {
const block: DeepnoteBlock = {
blockGroup: 'test-group',
content: '',
id: 'block-123',
metadata: {
deepnote_variable_name: 'range1',
deepnote_variable_value: ['', '']
},
sortingKey: 'a0',
type: 'input-date-range'
};
const cell = new NotebookCellData(NotebookCellKind.Code, '', 'python');

converter.applyChangesToBlock(block, cell);

assert.deepStrictEqual(block.metadata?.deepnote_variable_value, ['', '']);
});
});
});

Expand Down
Loading