Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
42fee8d
Refactor DateUtil to use grammar-based DateParser for date and dateti…
abdelaziz-mahdy Oct 31, 2025
e822526
feat(DateParser): enhance date parsing to allow 00-99 for minutes/sec…
abdelaziz-mahdy Nov 3, 2025
e11a0b9
Merge with dev
abdelaziz-mahdy Nov 3, 2025
06bfc9d
refactor(pom): remove unused SearchView parser from POM configuration
abdelaziz-mahdy Nov 3, 2025
b93bba5
refactor(DateParser): remove debug logging from parseDateTime method
abdelaziz-mahdy Nov 3, 2025
8099e3c
merge with dev
abdelaziz-mahdy Nov 3, 2025
521394f
merge with dev
abdelaziz-mahdy Nov 25, 2025
e05791d
chore: update code structure for improved readability and maintainabi…
abdelaziz-mahdy Nov 25, 2025
a49ddae
feat: enhance DateParser to support STRING mode for date parsing
abdelaziz-mahdy Nov 25, 2025
4ad9294
fix: update date parsing logic to allow single-digit values for day, …
abdelaziz-mahdy Nov 25, 2025
b9714a5
Merge With Dev
abdelaziz-mahdy Jan 20, 2026
eeb5db3
feat: add strict validation mode for date parsing with error handling
abdelaziz-mahdy Jan 20, 2026
3206a15
refactor: streamline date parser tests and enable strict validation mode
abdelaziz-mahdy Jan 20, 2026
9e58b82
feat: implement strict and non-strict validation modes for date parsing
abdelaziz-mahdy Jan 20, 2026
9a06c1a
fix: update error message for unsupported date format in strict mode
abdelaziz-mahdy Jan 20, 2026
c9eecbb
Merge branch 'development' of github.com:kgrgreer/foam3 into date-uti…
abdelaziz-mahdy Jan 20, 2026
b36ae4f
feat(date-util): implement lenient and strict validation modes for da…
abdelaziz-mahdy Jan 20, 2026
b71cbcd
feat(parse): add new date format tests for Java compatibility
abdelaziz-mahdy Jan 20, 2026
69e6fef
feat(parse): add Unix Date.toString() format and timestamp support …
abdelaziz-mahdy Jan 20, 2026
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
1,079 changes: 1,079 additions & 0 deletions src/foam/parse/DateParser.java

Large diffs are not rendered by default.

185 changes: 79 additions & 106 deletions src/foam/parse/DateParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,8 @@ foam.CLASS({
),
day2: str(seq(range('0', '3'), range('0', '9'))),
hour2: str(seq(range('0', '2'), range('0', '9'))),
minute2: str(seq(range('0', '5'), range('0', '9'))),
second2: str(seq(range('0', '5'), range('0', '9'))),
minute2: str(seq(range('0', '9'), range('0', '9'))), // Allow 00-99 for normalization
second2: str(seq(range('0', '9'), range('0', '9'))), // Allow 00-99 for normalization
millisecond3: str(repeat(range('0', '9'), null, 3, 3))
};
}
Expand Down Expand Up @@ -739,41 +739,10 @@ foam.CLASS({
}
},

{
name: 'validateDate',
documentation: 'Validates a date object (local time) and returns MAX_DATE for invalid dates',
code: function(date, str) {
// Check if date is NaN
if ( isNaN(date.getTime()) ) {
date = foam.Date.MAX_DATE;
console.warn("Invalid date: " + str + "; assuming " + date.toISOString() + ".");
return date;
}

// Allow JavaScript's native date normalization (e.g., 2025-13-01 → 2026-01-01)
return date;
}
},

{
name: 'validateDateUTC',
documentation: 'Validates a date object (UTC time) and returns MAX_DATE for invalid dates',
code: function(date, str) {
// Check if date is NaN
if ( isNaN(date.getTime()) ) {
date = foam.Date.MAX_DATE;
console.warn("Invalid date: " + str + "; assuming " + date.toISOString() + ".");
return date;
}

// Allow JavaScript's native date normalization (e.g., 2025-13-01 → 2026-01-01)
return date;
}
},

{
name: 'convertTwoDigitYear',
documentation: 'Converts 2-digit year using fixed pivot: 00-49 → 2000-2049, 50-99 → 1950-1999',
documentation: 'Converts 2-digit year using fixed pivot at 50: 00-49 → 2000-2049, 50-99 → 1950-1999',
code: function(twoDigitYear) {
// Fixed pivot at 50:
// Years 00-49 map to 2000-2049
Expand Down Expand Up @@ -802,16 +771,20 @@ foam.CLASS({

{
name: 'parseString',
documentation: 'Parse a date/datetime string and return a Date object. Auto-detects format and handles time if present. Returns MAX_DATE for invalid dates.',
documentation: 'Parse a date/datetime string and return a Date object. Auto-detects format and handles time if present. Throws exception for unsupported formats.',
code: function(str, opt_name) {
if ( ! str || str.trim() === '' ) {
throw new Error('Unsupported Date format: empty or null string');
}

let result = this.grammar_.parseString(str, opt_name || 'START');

if ( ! result ) {
// Unparseable format - return MAX_DATE
return this.validateDate(this.INVALID_DATE, str);
throw new Error('Unsupported Date format: ' + str);
}

// Determine if this is a datetime or date-only result based on presence of time components
// Let JavaScript Date normalize invalid dates (e.g., Feb 30 → Mar 2)
let ret;
if ( result.hour !== undefined || result.minute !== undefined || result.second !== undefined ) {
// Datetime format - use local time
Expand All @@ -829,72 +802,71 @@ foam.CLASS({
ret = new Date(Date.UTC(result.year, result.month, result.day, 12, 0, 0, 0));
}

return this.validateDate(ret, str);
if ( isNaN(ret.getTime()) ) {
throw new Error('Cannot parse invalid date: ' + str);
}

return ret;
}
},

{
name: 'parseDateString',
documentation: 'Parse a date string - ignores any time component and returns date at noon local time. Returns MAX_DATE for invalid dates.',
documentation: 'Parse a date string - ignores any time component and returns date at noon UTC. Throws exception for unsupported formats.',
code: function(str, opt_name) {
if ( ! str || str.trim() === '' ) {
throw new Error('Unsupported Date format: empty or null string');
}

let result = this.grammar_.parseString(str, opt_name || 'START');

if ( ! result ) {
// Unparseable format - return MAX_DATE
return this.validateDate(this.INVALID_DATE, str);
throw new Error('Unsupported Date format: ' + str);
}

// Always return date at noon UTC, ignoring time even if present
// Let JavaScript Date normalize invalid dates (e.g., Feb 30 → Mar 2)
let ret = new Date(Date.UTC(result.year, result.month, result.day, 12, 0, 0, 0));

return this.validateDate(ret, str);
if ( isNaN(ret.getTime()) ) {
throw new Error('Cannot parse invalid date: ' + str);
}

return ret;
}
},

{
name: 'parseDateTime',
documentation: 'Parse a datetime string using local time - uses time if present, otherwise sets to noon. If timezone is present, converts to UTC. Returns MAX_DATE for invalid dates.',
documentation: 'Parse a datetime string using local time - uses time if present, otherwise sets to noon. If timezone is present, converts to UTC. Throws exception for unsupported formats.',
code: function(str, opt_name) {
if ( ! str || str.trim() === '' ) {
throw new Error('Unsupported DateTime format: empty or null string');
}

// Trim input to remove leading/trailing whitespace
str = str ? str.trim() : str;
str = str.trim();

// Conditional logging for debugging specific test cases (minute 60 or hour 25)
var debugLog = str.indexOf('15:60:') >= 0 || str.indexOf('25:') >= 0;

// Use parse() instead of parseString() to get position information
this.grammar_.ps.setString(str);
let start = this.grammar_.getSymbol(opt_name || 'START');
let parseResult = this.grammar_.ps.apply(start, this.grammar_);

if ( ! parseResult ) {
// Unparseable format - return MAX_DATE
return this.validateDate(this.INVALID_DATE, str);
}

// Check if entire string was consumed
if ( parseResult.pos < str.length ) {
// Partial parse - remaining characters indicate invalid format
console.warn('DateParser: Partial parse detected. Input:', str, 'Consumed up to position:', parseResult.pos, 'Remaining:', str.substring(parseResult.pos));
return this.validateDate(this.INVALID_DATE, str);
if ( ! parseResult || ! parseResult.value ) {
throw new Error('Unsupported DateTime format: ' + str);
}

let result = parseResult.value;

if ( ! result ) {
// Unparseable format - return MAX_DATE
return this.validateDate(this.INVALID_DATE, str);
}

// Validate time components if present
// Note: Grammar already enforces valid ranges (hour2: 00-23, minute2/second2: 00-59)
// but we keep these checks as a safety measure
if ( result.hour !== undefined && (result.hour < 0 || result.hour > 23) ) {
return this.validateDate(this.INVALID_DATE, str);
}
if ( result.minute !== undefined && (result.minute < 0 || result.minute > 59) ) {
return this.validateDate(this.INVALID_DATE, str);
}
if ( result.second !== undefined && (result.second < 0 || result.second > 59) ) {
return this.validateDate(this.INVALID_DATE, str);
if ( debugLog ) {
console.log('[DateParser.parseDateTime] Input:', str);
console.log('[DateParser.parseDateTime] Parsed result:', JSON.stringify(result));
}

// Let JavaScript Date normalize invalid dates/times (e.g., Feb 30 → Mar 2, hour 25 → next day)
let ret;
if ( result.timezone ) {
// Timezone present - convert to UTC
Expand All @@ -911,10 +883,18 @@ foam.CLASS({
// Subtract offset to convert to UTC (if timezone is +05:00, we subtract 5 hours)
utcTime -= offset * 60000;
ret = new Date(utcTime);
// Don't validate date parts - timezone conversion is expected to change the date
return this.validateDate(ret, str);
if ( debugLog ) {
console.log('[DateParser.parseDateTime] Created UTC date, offset:', offset, 'minutes');
}
} else {
// No timezone - use local time
if ( debugLog ) {
console.log('[DateParser.parseDateTime] Creating local time date with:');
console.log(' year:', result.year, 'month:', result.month, 'day:', result.day);
console.log(' hour:', result.hour !== undefined ? result.hour : 12);
console.log(' minute:', result.minute !== undefined ? result.minute : 0);
console.log(' second:', result.second !== undefined ? result.second : 0);
}
ret = new Date(
result.year,
result.month,
Expand All @@ -924,55 +904,45 @@ foam.CLASS({
result.second !== undefined ? result.second : 0,
result.millisecond !== undefined ? result.millisecond : 0
);
return this.validateDate(ret, str);
if ( debugLog ) {
console.log('[DateParser.parseDateTime] Result date:');
console.log(' getHours():', ret.getHours(), 'getMinutes():', ret.getMinutes());
console.log(' getUTCHours():', ret.getUTCHours(), 'getUTCMinutes():', ret.getUTCMinutes());
console.log(' toString():', ret.toString());
}
}

if ( isNaN(ret.getTime()) ) {
throw new Error('Cannot parse invalid datetime: ' + str);
}

return ret;
}
},

{
name: 'parseDateTimeUTC',
documentation: 'Parse a datetime string using UTC time - uses time if present, otherwise sets to midnight. If timezone is present, converts to UTC. Returns MAX_DATE for invalid dates.',
documentation: 'Parse a datetime string using UTC time - uses time if present, otherwise sets to midnight. If timezone is present, converts to UTC. Throws exception for unsupported formats.',
code: function(str, opt_name) {
if ( ! str || str.trim() === '' ) {
throw new Error('Unsupported DateTime format: empty or null string');
}

// Trim input to remove leading/trailing whitespace
str = str ? str.trim() : str;
str = str.trim();

// Use parse() instead of parseString() to get position information
this.grammar_.ps.setString(str);
let start = this.grammar_.getSymbol(opt_name || 'START');
let parseResult = this.grammar_.ps.apply(start, this.grammar_);

if ( ! parseResult ) {
// Unparseable format - return MAX_DATE
return this.validateDateUTC(this.INVALID_DATE, str);
}

// Check if entire string was consumed
if ( parseResult.pos < str.length ) {
// Partial parse - remaining characters indicate invalid format
console.warn('DateParser: Partial parse detected for UTC. Input:', str, 'Consumed up to position:', parseResult.pos, 'Remaining:', str.substring(parseResult.pos));
return this.validateDateUTC(this.INVALID_DATE, str);
if ( ! parseResult || ! parseResult.value ) {
throw new Error('Unsupported DateTime format: ' + str);
}

let result = parseResult.value;

if ( ! result ) {
// Unparseable format - return MAX_DATE
return this.validateDateUTC(this.INVALID_DATE, str);
}

// Validate time components if present
// Note: Grammar already enforces valid ranges (hour2: 00-23, minute2/second2: 00-59)
// but we keep these checks as a safety measure
if ( result.hour !== undefined && (result.hour < 0 || result.hour > 23) ) {
return this.validateDateUTC(this.INVALID_DATE, str);
}
if ( result.minute !== undefined && (result.minute < 0 || result.minute > 59) ) {
return this.validateDateUTC(this.INVALID_DATE, str);
}
if ( result.second !== undefined && (result.second < 0 || result.second > 59) ) {
return this.validateDateUTC(this.INVALID_DATE, str);
}

// Let JavaScript Date normalize invalid dates/times (e.g., Feb 30 → Mar 2, hour 25 → next day)
let ret;
if ( result.timezone ) {
// Timezone present - convert to UTC
Expand All @@ -989,8 +959,6 @@ foam.CLASS({
// Subtract offset to convert to UTC (if timezone is +05:00, we subtract 5 hours)
utcTime -= offset * 60000;
ret = new Date(utcTime);
// Don't validate date parts - timezone conversion is expected to change the date
return this.validateDateUTC(ret, str);
} else {
// No timezone - use UTC time as-is
ret = new Date(Date.UTC(
Expand All @@ -1002,8 +970,13 @@ foam.CLASS({
result.second !== undefined ? result.second : 0,
result.millisecond !== undefined ? result.millisecond : 0
));
return this.validateDateUTC(ret, str);
}

if ( isNaN(ret.getTime()) ) {
throw new Error('Cannot parse invalid datetime: ' + str);
}

return ret;
}
}
]
Expand Down
Loading