Skip to content

Commit 04262dc

Browse files
Mike Greinerlazyguru
authored andcommitted
fix: handle numeric YAML parsing for startDate/endDate with YYYYMMDD format
Fixes issue #324 where dateFormat: YYYYMMDD made startDate and endDate ineffective, causing all files to be included regardless of date range. Root cause: YAML parsers may parse numeric-looking dates (e.g., 20240201) as numbers instead of strings. The code only checked typeof === 'string', so numeric values were skipped, leaving startDate/endDate as null. Changes: - Updated parsing.ts to handle both string and number types for startDate/endDate, converting numbers to strings before processing - Updated main.ts to use isBefore/isAfter with 'day' granularity for clearer, more readable date comparisons - Fixed closure issue by capturing startDate/endDate in local variables before async file processing loop - Fixed helper.ts bug: changed .contains() to .includes() for string method compatibility Tests: - Added unit tests for YYYYMMDD date format parsing and filtering - Added edge case tests for boundary conditions and missing dates - Added date comparison method tests - All 28 tests passing
1 parent 56db253 commit 04262dc

10 files changed

Lines changed: 957 additions & 15 deletions

src/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function makeTimeFormat() {
2424
if (fmtSec !== "") {
2525
fmt += `:${fmtSec}`;
2626
}
27-
if (fmtHour.contains("h")) {
27+
if (fmtHour.includes("h")) {
2828
fmt += " a";
2929
}
3030
timeFormat.push(fmt);

src/main.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ export default class Tracker extends Plugin {
304304
let dataMap: DataMap = new Map(); // {strDate: [query: value, ...]}
305305
let processInfo = new CollectingProcessInfo();
306306
processInfo.fileTotal = files.length;
307+
308+
// Store dates in local variables to ensure they're captured correctly in async closures
309+
// This fixes the issue where dates become null inside async functions
310+
const startDate = renderInfo.startDate ? renderInfo.startDate.clone() : null;
311+
const endDate = renderInfo.endDate ? renderInfo.endDate.clone() : null;
307312

308313
// Collect data from files, each file has one data point for each query
309314
const loopFilePromises = files.map(async (file) => {
@@ -428,15 +433,19 @@ export default class Tracker extends Plugin {
428433
skipThisFile = true;
429434
processInfo.fileNotInFormat++;
430435
} else {
431-
// console.log("file " + file.basename + " accepted");
432-
if (renderInfo.startDate !== null) {
433-
if (xDate < renderInfo.startDate) {
436+
// Date filtering: only include files within the specified date range
437+
// Use isBefore/isAfter with 'day' granularity for clear, readable date comparisons
438+
// Use local variables (startDate/endDate) captured before async loop to avoid closure issues
439+
if (startDate !== null && startDate.isValid()) {
440+
// Skip files with dates before the start date
441+
if (xDate.isBefore(startDate, 'day')) {
434442
skipThisFile = true;
435443
processInfo.fileOutOfDateRange++;
436444
}
437445
}
438-
if (renderInfo.endDate !== null) {
439-
if (xDate > renderInfo.endDate) {
446+
if (endDate !== null && endDate.isValid()) {
447+
// Skip files with dates after the end date
448+
if (xDate.isAfter(endDate, 'day')) {
440449
skipThisFile = true;
441450
processInfo.fileOutOfDateRange++;
442451
}
@@ -466,7 +475,9 @@ export default class Tracker extends Plugin {
466475
}
467476
}
468477
}
469-
if (skipThisFile) return;
478+
if (skipThisFile) {
479+
return;
480+
}
470481
// console.log(xValueMap);
471482
// console.log(`minDate: ${minDate}`);
472483
// console.log(`maxDate: ${maxDate}`);

src/parsing.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,14 +1280,21 @@ export function getRenderInfoFromYaml(
12801280

12811281
// startDate, endDate
12821282
// console.log("Parsing startDate");
1283-
if (typeof yaml.startDate === "string") {
1284-
if (/^([\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)m$/.test(yaml.startDate)) {
1283+
// Handle both string and number types (YAML may parse numeric dates as numbers)
1284+
if (typeof yaml.startDate === "string" || typeof yaml.startDate === "number") {
1285+
let strStartDate: string;
1286+
if (typeof yaml.startDate === "number") {
1287+
strStartDate = yaml.startDate.toString();
1288+
} else {
1289+
strStartDate = yaml.startDate;
1290+
}
1291+
if (/^([\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)m$/.test(strStartDate)) {
12851292
let errorMessage =
12861293
"'m' for 'minute' is too small for parameter startDate, please use 'd' for 'day' or 'M' for month";
12871294
return errorMessage;
12881295
}
1289-
let strStartDate = helper.getDateStringFromInputString(
1290-
yaml.startDate,
1296+
strStartDate = helper.getDateStringFromInputString(
1297+
strStartDate,
12911298
renderInfo.dateFormatPrefix,
12921299
renderInfo.dateFormatSuffix
12931300
);
@@ -1322,14 +1329,21 @@ export function getRenderInfoFromYaml(
13221329
}
13231330

13241331
// console.log("Parsing endDate");
1325-
if (typeof yaml.endDate === "string") {
1326-
if (/^([\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)m$/.test(yaml.endDate)) {
1332+
// Handle both string and number types (YAML may parse numeric dates as numbers)
1333+
if (typeof yaml.endDate === "string" || typeof yaml.endDate === "number") {
1334+
let strEndDate: string;
1335+
if (typeof yaml.endDate === "number") {
1336+
strEndDate = yaml.endDate.toString();
1337+
} else {
1338+
strEndDate = yaml.endDate;
1339+
}
1340+
if (/^([\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)m$/.test(strEndDate)) {
13271341
let errorMessage =
13281342
"'m' for 'minute' is too small for parameter endDate, please use 'd' for 'day' or 'M' for month";
13291343
return errorMessage;
13301344
}
1331-
let strEndDate = helper.getDateStringFromInputString(
1332-
yaml.endDate,
1345+
strEndDate = helper.getDateStringFromInputString(
1346+
strEndDate,
13331347
renderInfo.dateFormatPrefix,
13341348
renderInfo.dateFormatSuffix
13351349
);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Test to compare different date comparison methods
3+
*
4+
* Question: Is valueOf() the best method, or should we use isBefore/isAfter?
5+
*/
6+
7+
import * as helper from '../src/helper';
8+
import moment from 'moment';
9+
10+
describe('Date Comparison Methods', () => {
11+
beforeAll(() => {
12+
if (typeof (global as any).window === 'undefined') {
13+
(global as any).window = {};
14+
}
15+
(global as any).window.moment = moment;
16+
});
17+
18+
describe('valueOf() vs isBefore/isAfter', () => {
19+
it('should produce same results with valueOf() and isBefore/isAfter', () => {
20+
const startDate = helper.strToDate('20240201', 'YYYYMMDD');
21+
const endDate = helper.strToDate('20240203', 'YYYYMMDD');
22+
const testDate = helper.strToDate('20240202', 'YYYYMMDD');
23+
24+
// Method 1: valueOf() (current implementation)
25+
const inRangeValueOf =
26+
testDate.valueOf() >= startDate.valueOf() &&
27+
testDate.valueOf() <= endDate.valueOf();
28+
29+
// Method 2: isBefore/isAfter with 'day' granularity
30+
const inRangeMoment =
31+
!testDate.isBefore(startDate, 'day') &&
32+
!testDate.isAfter(endDate, 'day');
33+
34+
// Method 3: isSameOrAfter/isSameOrBefore
35+
const inRangeSame =
36+
testDate.isSameOrAfter(startDate, 'day') &&
37+
testDate.isSameOrBefore(endDate, 'day');
38+
39+
expect(inRangeValueOf).toBe(true);
40+
expect(inRangeMoment).toBe(true);
41+
expect(inRangeSame).toBe(true);
42+
});
43+
44+
it('should handle boundary dates correctly with both methods', () => {
45+
const startDate = helper.strToDate('20240201', 'YYYYMMDD');
46+
const endDate = helper.strToDate('20240203', 'YYYYMMDD');
47+
48+
// Test start date boundary
49+
const onStartDate = helper.strToDate('20240201', 'YYYYMMDD');
50+
expect(onStartDate.valueOf() >= startDate.valueOf()).toBe(true);
51+
expect(!onStartDate.isBefore(startDate, 'day')).toBe(true);
52+
expect(onStartDate.isSameOrAfter(startDate, 'day')).toBe(true);
53+
54+
// Test end date boundary
55+
const onEndDate = helper.strToDate('20240203', 'YYYYMMDD');
56+
expect(onEndDate.valueOf() <= endDate.valueOf()).toBe(true);
57+
expect(!onEndDate.isAfter(endDate, 'day')).toBe(true);
58+
expect(onEndDate.isSameOrBefore(endDate, 'day')).toBe(true);
59+
});
60+
61+
it('should handle dates normalized to start of day', () => {
62+
// strToDate normalizes to startOf('day'), so all dates should be at midnight
63+
const date1 = helper.strToDate('20240201', 'YYYYMMDD');
64+
const date2 = helper.strToDate('20240201', 'YYYYMMDD');
65+
66+
// Both should be at midnight (00:00:00)
67+
expect(date1.hours()).toBe(0);
68+
expect(date1.minutes()).toBe(0);
69+
expect(date1.seconds()).toBe(0);
70+
expect(date1.valueOf()).toBe(date2.valueOf());
71+
});
72+
});
73+
74+
describe('Performance and clarity', () => {
75+
it('should note that isBefore/isAfter is more explicit about day granularity', () => {
76+
// isBefore/isAfter with 'day' is more explicit about what we're comparing
77+
// valueOf() works but is less clear about granularity
78+
const date1 = helper.strToDate('20240201', 'YYYYMMDD');
79+
const date2 = helper.strToDate('20240201', 'YYYYMMDD');
80+
81+
// Both methods work, but isBefore with 'day' is more explicit
82+
expect(date1.isBefore(date2, 'day')).toBe(false);
83+
expect(date1.valueOf() < date2.valueOf()).toBe(false);
84+
});
85+
});
86+
});

0 commit comments

Comments
 (0)