ken_nogi/NodeJS/アルファオフィススケジュール/icalConvert.js
Kenichiro NOGI 88a402ce0f up
2026-07-10 18:13:30 +09:00

157 lines
5.1 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { parse } = require('csv-parse/sync');
const REQUIRED_HEADERS = ['登録先', '開始日', '開始時刻', '終了日', '終了時刻', '分類', '予定', '備考'];
const INPUT_DIR = path.join(__dirname, 'data');
const OUTPUT_DIR = path.join(__dirname, 'ical');
function formatDateTime(dateText, timeText) {
const [year, month, day] = String(dateText || '').split('/').map(Number);
const [hour, minute] = String(timeText || '').split(':').map(Number);
if (!year || !month || !day || Number.isNaN(hour) || Number.isNaN(minute)) {
throw new Error(`日時形式が不正です: ${dateText} ${timeText}`);
}
const yyyy = String(year).padStart(4, '0');
const MM = String(month).padStart(2, '0');
const dd = String(day).padStart(2, '0');
const HH = String(hour).padStart(2, '0');
const mm = String(minute).padStart(2, '0');
return `${yyyy}${MM}${dd}T${HH}${mm}00`;
}
function getUtcStamp() {
const now = new Date();
const yyyy = String(now.getUTCFullYear()).padStart(4, '0');
const MM = String(now.getUTCMonth() + 1).padStart(2, '0');
const dd = String(now.getUTCDate()).padStart(2, '0');
const HH = String(now.getUTCHours()).padStart(2, '0');
const mm = String(now.getUTCMinutes()).padStart(2, '0');
const ss = String(now.getUTCSeconds()).padStart(2, '0');
return `${yyyy}${MM}${dd}T${HH}${mm}${ss}Z`;
}
function escapeIcalText(value) {
return String(value || '')
.replace(/\\/g, '\\\\')
.replace(/\r\n|\n|\r/g, '\\n')
.replace(/,/g, '\\,')
.replace(/;/g, '\\;');
}
function validateHeaders(headers) {
const normalized = headers.map(v => String(v || '').trim());
const invalid = REQUIRED_HEADERS.some((expected, index) => normalized[index] !== expected);
if (invalid) {
throw new Error(`ヘッダー不一致です。期待: ${REQUIRED_HEADERS.join(', ')}`);
}
}
function createIcsEvent(record, index) {
const start = formatDateTime(record['開始日'], record['開始時刻']);
const end = formatDateTime(record['終了日'], record['終了時刻']);
const summary = escapeIcalText(record['予定']);
const descriptionParts = [];
//if (record['登録先']) descriptionParts.push(`登録先: ${record['登録先']}`);
if (record['分類']) descriptionParts.push(`分類: ${record['分類']}`);
if (record['備考']) descriptionParts.push(`備考: ${record['備考']}`);
const description = escapeIcalText(descriptionParts.join('\n'));
const uid = `${Date.now()}-${index}-${Math.random().toString(36).slice(2)}@alpha-office`;
let eventText = 'BEGIN:VEVENT\n';
eventText += `UID:${uid}\n`;
eventText += `DTSTAMP:${getUtcStamp()}\n`;
eventText += `DTSTART;TZID=Asia/Tokyo:${start}\n`;
eventText += `DTEND;TZID=Asia/Tokyo:${end}\n`;
eventText += `SUMMARY:${summary}\n`;
eventText += `DESCRIPTION:${description}\n`;
eventText += 'END:VEVENT\n';
return eventText;
}
function csvToIcal(csvFileName) {
try {
const csvPath = path.join(INPUT_DIR, csvFileName);
const csvContent = fs.readFileSync(csvPath, 'utf-8');
// ダブルクォーテーションと引用符内改行を含むCSVを正しく解析
const records = parse(csvContent, {
columns: true,
skip_empty_lines: true,
bom: true
});
if (records.length === 0) {
throw new Error('データ行がありません');
}
validateHeaders(Object.keys(records[0]));
let ical = 'BEGIN:VCALENDAR\n';
ical += 'VERSION:2.0\n';
ical += 'PRODID:-//Alpha Office Schedule//JP\n';
ical += 'CALSCALE:GREGORIAN\n';
ical += 'METHOD:PUBLISH\n';
records.forEach((record, index) => {
ical += createIcsEvent(record, index);
});
ical += 'END:VCALENDAR\n';
const outputPath = path.join(OUTPUT_DIR, `${path.basename(csvFileName, '.csv')}.ics`);
fs.writeFileSync(outputPath, ical, 'utf-8');
console.log(`✓ 変換完了: ${outputPath}`);
return true;
} catch (error) {
console.error(`✗ 変換失敗 (${csvFileName}):`, error.message);
return false;
}
}
function main() {
if (!fs.existsSync(INPUT_DIR)) {
console.error(`入力フォルダが見つかりません: ${INPUT_DIR}`);
process.exitCode = 1;
return;
}
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
const files = fs.readdirSync(INPUT_DIR)
.filter(file => file.toLowerCase().endsWith('.csv'))
.sort();
if (files.length === 0) {
console.log('csvフォルダにCSVファイルがありません');
return;
}
let successCount = 0;
let failCount = 0;
files.forEach(file => {
if (csvToIcal(file)) {
successCount += 1;
} else {
failCount += 1;
}
});
console.log(`処理結果: 成功 ${successCount} / 失敗 ${failCount}`);
if (failCount > 0) {
process.exitCode = 1;
}
}
main();