Skip to content

Instantly share code, notes, and snippets.

@samber
Created October 21, 2025 22:43
Show Gist options
  • Save samber/069d631da03d21bf16b69cee225eb41e to your computer and use it in GitHub Desktop.
Save samber/069d631da03d21bf16b69cee225eb41e to your computer and use it in GitHub Desktop.
Find updates or removal in 2 ICS files
const ical = require('node-ical');
const moment = require('moment');
const fs = require('fs');
const CUTOFF_DATE = moment('2024-07-13');
const parseIcsFile = async (filePath) => {
try {
const data = fs.readFileSync(filePath, 'utf8');
const events = ical.parseICS(data);
const parsedEvents = [];
for (const [key, event] of Object.entries(events)) {
if (event.type === 'VEVENT') {
parsedEvents.push({
uid: event.uid || key,
summary: event.summary || 'No Title',
description: event.description || '',
start: moment(event.start),
end: moment(event.end),
location: event.location || '',
created: event.created ? moment(event.created) : null,
lastModified: event.lastmodified ? moment(event.lastmodified) : null,
sequence: event.sequence || 0,
rawData: event
});
}
}
return parsedEvents;
} catch (error) {
console.error(`Error parsing file ${filePath}:`, error.message);
return [];
}
};
const normalizeEventForComparison = (event) => ({
uid: event.uid,
summary: event.summary,
start: event.start.format(),
end: event.end.format(),
location: event.location,
description: event.description,
lastModified: event.lastModified ? event.lastModified.format() : null
});
const isEventBeforeCutoff = (event) => event.start.isBefore(CUTOFF_DATE);
const compareEvents = (oldEvents, newEvents) => {
const oldEventMap = new Map();
const newEventMap = new Map();
// Create maps for easier lookup
oldEvents
.filter(isEventBeforeCutoff)
.forEach(event => oldEventMap.set(event.uid, event));
newEvents.forEach(event => newEventMap.set(event.uid, event));
const results = {
removedEvents: [],
updatedEvents: [],
unchangedEvents: []
};
// Check for removed and updated events
for (const [uid, oldEvent] of oldEventMap) {
const newEvent = newEventMap.get(uid);
if (!newEvent) {
// Event was removed
results.removedEvents.push({
uid: oldEvent.uid,
summary: oldEvent.summary,
start: oldEvent.start.format(),
end: oldEvent.end.format(),
location: oldEvent.location,
description: oldEvent.description,
created: oldEvent.created ? oldEvent.created.format() : null,
lastModified: oldEvent.lastModified ? oldEvent.lastModified.format() : null
});
} else {
// Check if event was updated
const oldNormalized = normalizeEventForComparison(oldEvent);
const newNormalized = normalizeEventForComparison(newEvent);
const isUpdated =
oldNormalized.summary !== newNormalized.summary ||
oldNormalized.start !== newNormalized.start ||
oldNormalized.end !== newNormalized.end ||
oldNormalized.location !== newNormalized.location ||
oldNormalized.description !== newNormalized.description ||
oldEvent.sequence !== newEvent.sequence ||
oldNormalized.lastModified !== oldNormalized.lastModified;
if (isUpdated) {
results.updatedEvents.push({
uid: uid,
summary: oldEvent.summary,
changes: {
old: {
summary: oldEvent.summary,
start: oldEvent.start.format(),
end: oldEvent.end.format(),
location: oldEvent.location,
description: oldEvent.description,
lastModified: oldEvent.lastModified ? oldEvent.lastModified.format() : null,
sequence: oldEvent.sequence
},
new: {
summary: newEvent.summary,
start: newEvent.start.format(),
end: newEvent.end.format(),
location: newEvent.location,
description: newEvent.description,
lastModified: newEvent.lastModified ? newEvent.lastModified.format() : null,
sequence: newEvent.sequence
}
}
});
} else {
results.unchangedEvents.push({
uid: uid,
summary: oldEvent.summary,
start: oldEvent.start.format()
});
}
}
}
return results;
};
const displayResults = (results) => {
console.log('\n=== COMPARISON RESULTS ===');
console.log(`Cutoff date: ${CUTOFF_DATE.format('YYYY-MM-DD')}`);
console.log(`\nπŸ—‘οΈ REMOVED EVENTS: ${results.removedEvents.length}`);
if (results.removedEvents.length > 0) {
results.removedEvents.forEach((event, index) => {
console.log(`\n${index + 1}. ${event.summary}`);
console.log(` UID: ${event.uid}`);
console.log(` Start: ${event.start}`);
console.log(` End: ${event.end}`);
if (event.location) console.log(` Location: ${event.location}`);
if (event.description) console.log(` Description: ${event.description.substring(0, 100)}${event.description.length > 100 ? '...' : ''}`);
if (event.lastModified) console.log(` Last Modified: ${event.lastModified}`);
});
}
console.log(`\n✏️ UPDATED EVENTS: ${results.updatedEvents.length}`);
if (results.updatedEvents.length > 0) {
results.updatedEvents.forEach((event, index) => {
console.log(`\n${index + 1}. ${event.summary} (UID: ${event.uid})`);
const changes = event.changes;
if (changes.old.summary !== changes.new.summary) {
console.log(` Title: "${changes.old.summary}" β†’ "${changes.new.summary}"`);
}
if (changes.old.start !== changes.new.start) {
console.log(` Start: ${changes.old.start} β†’ ${changes.new.start}`);
}
if (changes.old.end !== changes.new.end) {
console.log(` End: ${changes.old.end} β†’ ${changes.new.end}`);
}
if (changes.old.location !== changes.new.location) {
console.log(` Location: "${changes.old.location}" β†’ "${changes.new.location}"`);
}
if (changes.old.description !== changes.new.description) {
console.log(` Description: Updated`);
}
if (changes.old.sequence !== changes.new.sequence) {
console.log(` Sequence: ${changes.old.sequence} β†’ ${changes.new.sequence}`);
}
});
}
console.log(`\nβœ… UNCHANGED EVENTS: ${results.unchangedEvents.length}`);
console.log(`\nπŸ“Š SUMMARY:`);
console.log(` Total events before cutoff: ${results.removedEvents.length + results.updatedEvents.length + results.unchangedEvents.length}`);
console.log(` Removed: ${results.removedEvents.length}`);
console.log(` Updated: ${results.updatedEvents.length}`);
console.log(` Unchanged: ${results.unchangedEvents.length}`);
};
const compareFiles = async (oldFilePath, newFilePath) => {
console.log('Parsing old calendar file...');
const oldEvents = await parseIcsFile(oldFilePath);
console.log(`Found ${oldEvents.length} events in old file`);
console.log('Parsing new calendar file...');
const newEvents = await parseIcsFile(newFilePath);
console.log(`Found ${newEvents.length} events in new file`);
console.log('\nComparing events before cutoff date (2024-07-13)...');
const results = compareEvents(oldEvents, newEvents);
displayResults(results);
return results;
};
const validateFiles = (oldFile, newFile) => {
if (!fs.existsSync(oldFile)) {
console.error(`Error: File not found: ${oldFile}`);
return false;
}
if (!fs.existsSync(newFile)) {
console.error(`Error: File not found: ${newFile}`);
return false;
}
return true;
};
const main = async () => {
const oldFile = process.argv[2];
const newFile = process.argv[3];
if (!oldFile || !newFile) {
console.log('Usage: node compare.js <old_ics_file> <new_ics_file>');
console.log('Example: node compare.js calendar_2024-07-13.ics calendar_2025-02-08.ics');
process.exit(1);
}
if (!validateFiles(oldFile, newFile)) {
process.exit(1);
}
await compareFiles(oldFile, newFile);
};
// Run the script
if (require.main === module) {
main().catch(console.error);
}
module.exports = {
parseIcsFile,
compareEvents,
compareFiles,
displayResults,
isEventBeforeCutoff,
normalizeEventForComparison
};
{
"name": "ics-comparator",
"version": "1.0.0",
"description": "Compare two .ics files to detect removed or updated events",
"main": "compare.js",
"scripts": {
"start": "node compare.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"node-ical": "^0.18.0",
"moment": "^2.29.4"
},
"keywords": ["ics", "calendar", "compare", "events"],
"author": "",
"license": "ISC"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment