Skip to content

Add iCalendar (.ics) to CSV conversion and back (#340) - #381

Merged
KnifeLemon merged 3 commits into
KnifeLemon:mainfrom
SeoJCalive:feature/ics-calendar
Oct 23, 2026
Merged

KnifeLemon merged 3 commits into
KnifeLemon:mainfrom
SeoJCalive:feature/ics-calendar

Conversation

@SeoJCalive

@SeoJCalive SeoJCalive commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

What does this change?

Closes #340. Following your answer in #340, iCalendar files now convert to CSV and JSON, and CSV converts back to iCalendar. It plugs into DataConverter the same way vCard does, with its own parser (Data/ICalendar.cs).

  • Reading (ics → CSV): each event (VEVENT) and to-do (VTODO) becomes one row with 14 columns.
    • Columns, in order: Type, Title, Start, End, All day, Time zone, Location, Description, Organizer, Attendees, RRULE, EXDATE, Recurrence ID, UID.
    • To-dos are in the same table with Type = todo. Their end comes from DUE.
    • Several attendees, and several EXDATE lines, are joined with line breaks inside one cell.
    • VALARM, X- properties, VJOURNAL and the like are skipped.
  • Times:
    • ISO 8601 with the original offset (2026-10-27T14:30:00+09:00). UTC values keep their Z.
    • All-day events get the date only.
    • The TZID goes into its own Time zone column.
    • The offset is looked up in this order:
      1. the transitions of a VTIMEZONE without RRULE in the same VCALENDAR
      2. the system time-zone database (IANA and Windows names)
      3. otherwise the local time is kept without an offset
  • Recurrence: RRULE is kept as its value. EXDATE and RECURRENCE-ID are kept as whole content lines. Nothing is expanded.
  • JSON and other formats: they go through CSV (ics → CSV → JSON, and the same for YAML, XML and so on). The only new edges are ics ↔ csv.
  • Writing (CSV → ics): like vCard, the 14 columns above are read back and written as VEVENTs and VTODOs.
    • Headers are case-insensitive, and common aliases work (Summary, Subject, 제목, 开始 …).
    • Each VTIMEZONE is built from the system zone's actual transitions. .NET's adjustment rules come out as per-year fixed dates on Linux, so they are not used.
    • Lines are folded at 75 octets, and the file has no BOM. A row without a UID gets one derived from its contents.
    • Rows in the canonical 14-column shape come back character for character after CSV → ics → CSV.
  • Donuts and settings migration (schema 33):
    • ics and ical are added to the data donut.
    • An "iCalendar" preset (CSV → ICS) goes into the slot after vCard on the back of the spreadsheets donut.
    • The migration leaves the user's choices alone:
      • an extension that another donut already claims stays there
      • a library whose data donut was deleted does not get it back
      • when the back face is full, the preset is only added to the preset list
  • Texts: the vCard sentence of engine.data.description now reads "vCard contacts and iCalendar events to CSV and back." in all 10 languages. There are no new keys. The data row in README.md and the engine table and data section in docs/ENGINES.md are updated too.

How was it tested?

  • dotnet format --verify-no-changes: passes
  • dotnet test
    • Core 457/457
    • Engines ICalendar, VCard and DataTests 48/48 (23 new tests in ICalendarTests)
    • App Localization 45/45
    • The Release build has the same 10 warnings as main. None come from this change.
  • I checked that the tests actually catch mistakes with 9 deliberate breakages. Each one made at least one test fail:
    • swapping the time-zone lookup order
    • looking up a VTIMEZONE from another VCALENDAR
    • changing the EXDATE and attendee separators
    • changing the local-time rule when writing
    • removing the migration block
    • removing the back-face insertion
    • leaving out the VTIMEZONE
    • folding at 76 octets
  • Tried it in the app (describe): I checked the CLI (filee convert) with files exported from real calendar apps.
    • Apple Calendar (macOS 26.6.2):
      1. I made a test calendar with an all-day event, a timed event, a weekly event with one excluded day, and an event with a comma and semicolon in the title and a Korean note, then exported it.
      2. The --to csv and --to json results matched what the app shows.
      3. I turned that CSV into an .ics with --to ics and imported it into a new calendar. All 4 events kept their times, all-day flag, recurrence (4 times, 1 excluded), location and note.
    • Google Calendar:
      1. I exported only a test calendar and checked it the same way.
      2. Importing the CSV → ics result into a new calendar reported "4 of 4 events imported".
      3. Exporting that again and converting it to CSV gave the same CSV as the first time.
    • I deleted all the test calendars afterwards.

Screenshots (UI changes)

(Not applicable) The only visible changes are one "iCalendar" chip on the back of the spreadsheets donut and the engine description. Instead, here is the conversion of a file exported from Google Calendar:

$ filee convert google.ics --to csv
Type,Title,Start,End,All day,Time zone,Location,Description,Organizer,Attendees,RRULE,EXDATE,Recurrence ID,UID
event,GQ weekly,2026-10-26T09:00:00+09:00,2026-10-26T09:30:00+09:00,false,Asia/Seoul,,,,,FREQ=WEEKLY;COUNT=4,EXDATE;TZID=Asia/Seoul:20261102T090000,,fqa-weekly@filee.test
event,GQ all day,2026-10-28,2026-10-29,true,,,,,,,,,fqa-allday@filee.test
event,"GQ timed, Seoul",2026-10-27T05:30:00Z,2026-10-27T06:15:00Z,false,,Room 3,"line one
line two 한글",,,,,,fqa-timed@filee.test
event,GQ New York,2026-11-05T19:00:00Z,2026-11-05T20:00:00Z,false,,,,,,,,,fqa-ny@filee.test

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files (no new keys; the existing sentence is updated in all 10 languages)
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (not applicable, no new libraries)

Points I'd like you to check

1. Schema number

2. Overlap with #351

3. Google exports one-off events in UTC

  • Google Calendar writes timed events that don't repeat as UTC, like DTSTART:20261027T053000Z.
  • Following "keep the original offset", the CSV shows the Z time and an empty Time zone. Recurring events keep their TZID and come out as +09:00.
  • I thought it better not to guess a time zone the file doesn't state, but let me know if you'd prefer something else.

4. Known limits (also written in docs/ENGINES.md)

  • A VTIMEZONE with an RRULE is resolved through the system time-zone database instead of the file's rule, so past years may differ.
  • When writing CSV → ics, each VTIMEZONE follows the rule in force at that zone's earliest time.
  • I left attendees out of the real-app check because importing them could send invitation emails. Attendees are covered by the synthetic tests only.

- Read VEVENT and VTODO into 14 CSV columns: ISO 8601 times with the
  original offset, all-day as dates, TZID in its own column, RRULE,
  EXDATE and RECURRENCE-ID kept raw; JSON and other data formats via CSV.
- Resolve offsets from a same-calendar VTIMEZONE without RRULE first,
  then the system time-zone database, else keep floating time.
- Write CSV back to iCalendar (VEVENT/VTODO, VTIMEZONE built from the
  zone's real transitions, 75-octet folding, no BOM).
- Register ics/ical on the data donut, add an "iCalendar" preset on the
  back of the spreadsheets donut (settings schema 33), update the data
  engine description in every UI language and the docs.

Closes KnifeLemon#340
@SeoJCalive SeoJCalive changed the title 제목: iCalendar(.ics) ↔ CSV 변환 추가 (#340) iCalendar(.ics) ↔ CSV 변환 추가 (#340) Oct 22, 2026
@SeoJCalive SeoJCalive changed the title iCalendar(.ics) ↔ CSV 변환 추가 (#340) Add iCalendar (.ics) to CSV conversion and back (#340) Oct 22, 2026
Windows has no history for Nepal Standard Time (always +05:45), so the
1985 +05:30 case failed there. Moscow (+04:00 in 2013, +03:00 now) has
the same history on Windows and in the IANA database.
@KnifeLemon

Copy link
Copy Markdown
Owner

@SeoJCalive 님, 정말 감사합니다. #340에서 합의한 내용을 그대로 구현해 주셨고, 실제 Apple·Google 캘린더로 왕복 테스트까지 해 주셔서 큰 도움이 됐습니다. CI는 초록이고, 로컬에서도 format, 빌드, Core, 데이터 엔진 테스트(94개), 로컬라이제이션 테스트가 모두 통과했습니다.

질문 주신 부분에 답변드립니다.

  1. 스키마: #351이 먼저 들어가서 33을 쓸 가능성이 높습니다. 리베이스하실 때 다음 빈 번호로 옮겨 주세요. #351이 머지되면 몇 번인지 알려드리겠습니다.
  2. #351 / #359 / #334 / #387: 네, #351 이후에 리베이스해 주시고, 이 PR보다 먼저 들어온 새 언어 파일에도 해당 문장을 추가해 주세요.
  3. Google에서 온 UTC 단일 일정: Z를 유지하고 Time zone을 비워 두는 방식이 맞습니다. 파일에 적혀 있지 않은 시간대를 추측하지 않는 게 좋습니다.
  4. 문서에 적어 주신 알려진 제약은 그대로 괜찮습니다.

Draft를 풀기 전에 하나만 고쳐 주세요. CSV → ICS 변환에서 일부 셀이 그대로 출력됩니다. UID나 RRULE에 줄바꿈이 있거나, EXDATE/Recurrence ID에 EXDATE/RECURRENCE-ID가 아닌 줄이 있으면 그 줄이 별도의 콘텐츠 줄로 들어갑니다. 실제로 RRULE 셀에 BEGIN:VALARM을, EXDATE 셀에 END:VEVENT / BEGIN:VEVENT / SUMMARY:Injected를 넣은 CSV로 시험해 보니, 만들어진 .ics에 일정이 하나 더 생기고 VALARM은 닫히지 않았습니다. CSV는 다른 사람에게서 받아 캘린더로 가져올 수도 있으니 아래처럼 부탁드립니다.

  • UID, RRULE, Time zone 값에서 CR/LF를 제거하거나 거부해 주세요. TZID에 :, ;, "가 들어 있으면 거부해 주세요.
  • 그 두 열에서는 EXDATE… / RECURRENCE-ID… 줄만 쓰고, 나머지는 건너뛰어 주세요.
  • 참석자 표시 이름은 CN=에 따옴표로 넣기 전에 "를 이스케이프하거나 제거해 주세요.

이 경우에 대한 테스트와 스키마 번호만 정리해서 Ready로 바꿔 주시면 머지하겠습니다. 다시 한번 감사드립니다!

A CSV cell could break out of its property when converting CSV to ICS:
a line break in UID or RRULE, or extra lines in the EXDATE and
Recurrence ID columns, became content lines of their own (an extra
VEVENT, an unclosed VALARM).

- UID, RRULE and Time zone with CR or LF are dropped (UID falls back to
  the generated one); a time zone with ':', ';' or '"' is dropped too.
- EXDATE and Recurrence ID columns keep only EXDATE / RECURRENCE-ID
  lines.
- Organizer and attendee cells lose CR/LF, and quotes are removed from
  the CN display name.

Filee's own ICS -> CSV output writes those three cells on one line, so
round trips are unchanged.
@SeoJCalive

Copy link
Copy Markdown
Collaborator Author

@KnifeLemon 님, 직접 만든 CSV로 시험해 주셔서 감사합니다. 말씀대로 CSV → ICS 쪽이 그 셀들을 그대로 믿고 있었습니다. 1c06463에서 고쳤습니다.

  • UID, RRULE, Time zone 값에 CR/LF가 있으면 그 값을 버립니다. 일정 자체는 그대로 쓰고, UID는 자동 생성 값으로 대신합니다. Time zone에 :, ;, "가 있어도 같은 방식으로 버려서 TZID에 들어가지 않습니다.
  • EXDATE와 Recurrence ID 열에서는 EXDATE… / RECURRENCE-ID…로 시작하는 줄만 쓰고, 나머지 줄은 건너뜁니다.
  • 참석자·주최자 표시 이름은 CN=에 넣기 전에 "를 제거하고, 같은 종류의 문제라 이 셀들의 줄바꿈도 함께 제거했습니다.
  • 테스트는 말씀하신 두 경우(RRULE에 BEGIN:VALARM, EXDATE에 END:VEVENT / BEGIN:VEVENT / SUMMARY:Injected)에서 행마다 VEVENT가 정확히 하나이고, 모든 BEGIN에 END가 짝지어지며, 예상한 속성 이름만 나오는지 확인합니다. 버려지는 값과 따옴표가 든 표시 이름도 각각 확인합니다.

Filee의 ICS → CSV 출력은 UID, RRULE, Time zone을 한 줄로 쓰기 때문에, 왕복 변환 결과는 달라지지 않습니다.

#351이 머지되면 리베이스해서 알려 주시는 다음 빈 스키마 번호로 옮기고, 먼저 들어온 언어 파일(zh-TW, fr 등)에도 문장을 추가한 뒤 Ready로 바꾸겠습니다.

@KnifeLemon

Copy link
Copy Markdown
Owner

@SeoJCalive 님, 1c06463 확인했어요. 말씀드린 경우가 모두 막혔고, 테스트도 딱 필요한 부분을 확인하네요. 빠르게 고쳐 주셔서 감사합니다!

#351을 기다리실 필요는 없어요. 스키마 번호와 그사이 들어온 프랑스어·번체 중국어 파일의 문장은 머지할 때 저희가 맞출게요. 준비되시면 Draft만 풀어 주세요.

@flexeykinDev flexeykinDev mentioned this pull request Oct 22, 2026
5 tasks done
@SeoJCalive
SeoJCalive marked this pull request as ready for review October 22, 2026 12:40
@SeoJCalive

Copy link
Copy Markdown
Collaborator Author

@KnifeLemon님, 감사합니다 Draft는 풀어놓았습니다!

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Draft 해제 확인했어요. 감사합니다! 머지할 때 스키마 번호와 프랑스어·번체 중국어·네팔어 문장은 저희가 맞출게요.

@KnifeLemon
KnifeLemon merged commit 39538f9 into KnifeLemon:main Oct 23, 2026
9 checks passed
KnifeLemon added a commit that referenced this pull request Oct 23, 2026
Merge #393, #432, #381 and #349 with their settings migrations renumbered
@KnifeLemon

Copy link
Copy Markdown
Owner

@SeoJCalive 님, #464로 머지됐어요. 감사합니다! 설정 마이그레이션은 스키마 36이 됐고, 그사이 들어온 프랑스어·번체 중국어·네팔어 파일의 데이터 엔진 설명도 새 문장에 맞췄어요. 다음 릴리스에 들어가요.

KnifeLemon added a commit to cre8ive-marketing/Filee that referenced this pull request Oct 23, 2026
…ne text for fr, zh-TW, ne

The iCalendar step becomes v36 after KnifeLemon#432's v35, AppSettings.CurrentSchemaVersion goes
to 36 and the iCalendar migration test starts from 35.

engine.data.description now names iCalendar events in English; French and Traditional
Chinese still said vCard only, and Nepali had an older one-line text. All three now say
what the English one says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KnifeLemon added a commit to cre8ive-marketing/Filee that referenced this pull request Oct 23, 2026
… zh-TW, ne

The everyday-tools step becomes v37 after KnifeLemon#381's v36, AppSettings.CurrentSchemaVersion
goes to 37, its offers table is Version37Offers, and the migration tests start from 36.

French, Traditional Chinese and Nepali get the twelve new strings (the media tool
preset names, the slideshow options and the too-many-pictures error) in en.json's
order, using each file's existing terms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SeoJCalive
SeoJCalive deleted the feature/ics-calendar branch October 23, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features formats New formats and engines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Format request: .ics (iCalendar) to CSV/JSON

2 participants