157 lines
5.1 KiB
JavaScript
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();
|