This commit is contained in:
2025-08-30 10:25:01 +02:00
parent 79d048f2f8
commit 31deff9809
9 changed files with 703 additions and 3 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,2 +1,343 @@
// See https://aka.ms/new-console-template for more information using System.Globalization;
Console.WriteLine("Hello, World!"); using System.Text;
using System.Text.RegularExpressions;
namespace Org2Ics;
class Program
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: Org2Ics <input.org|input.ics> [output.ics|output.org]");
Console.WriteLine("Converts between .org diary files and .ics calendar files.");
Console.WriteLine(" - .org to .ics: Converts org diary entries to calendar events");
Console.WriteLine(" - .ics to .org: Converts calendar events to org diary format");
return;
}
string inputFile = args[0];
string inputExt = Path.GetExtension(inputFile).ToLowerInvariant();
string outputFile;
if (args.Length > 1)
{
outputFile = args[1];
}
else
{
// Auto-determine output format based on input
outputFile = inputExt == ".org"
? Path.ChangeExtension(inputFile, ".ics")
: Path.ChangeExtension(inputFile, ".org");
}
try
{
var converter = new CalendarConverter();
if (inputExt == ".org")
{
converter.ConvertOrgToIcs(inputFile, outputFile);
}
else if (inputExt == ".ics")
{
converter.ConvertIcsToOrg(inputFile, outputFile);
}
else
{
throw new ArgumentException($"Unsupported file format: {inputExt}. Only .org and .ics files are supported.");
}
Console.WriteLine($"Successfully converted {inputFile} to {outputFile}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
public class CalendarConverter
{
private readonly Regex _dateHeaderRegex = new(@"^\*{3}\s+(\d{4}-\d{2}-\d{2})\s*$", RegexOptions.Compiled);
public void ConvertOrgToIcs(string inputFile, string outputFile)
{
if (!File.Exists(inputFile))
throw new FileNotFoundException($"Input file not found: {inputFile}");
var entries = ParseOrgFile(inputFile);
GenerateIcsFile(entries, outputFile);
}
public void ConvertIcsToOrg(string inputFile, string outputFile)
{
if (!File.Exists(inputFile))
throw new FileNotFoundException($"Input file not found: {inputFile}");
var entries = ParseIcsFile(inputFile);
GenerateOrgFile(entries, outputFile);
}
private List<DiaryEntry> ParseOrgFile(string inputFile)
{
var entries = new List<DiaryEntry>();
var lines = File.ReadAllLines(inputFile);
DiaryEntry? currentEntry = null;
var contentBuilder = new StringBuilder();
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
var match = _dateHeaderRegex.Match(line);
if (match.Success)
{
// Save previous entry if exists
if (currentEntry != null)
{
currentEntry.Content = contentBuilder.ToString().Trim();
entries.Add(currentEntry);
contentBuilder.Clear();
}
// Start new entry
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date))
{
currentEntry = new DiaryEntry
{
Date = date,
Content = string.Empty
};
}
}
else if (currentEntry != null)
{
// Check if this is another header that ends the current entry
if (line.StartsWith("*"))
{
// This is another header, save current entry and reset
currentEntry.Content = contentBuilder.ToString().Trim();
entries.Add(currentEntry);
currentEntry = null;
contentBuilder.Clear();
}
else
{
// Add content to current entry
contentBuilder.AppendLine(line);
}
}
}
// Save the last entry if exists
if (currentEntry != null)
{
currentEntry.Content = contentBuilder.ToString().Trim();
entries.Add(currentEntry);
}
return entries;
}
private void GenerateIcsFile(List<DiaryEntry> entries, string outputFile)
{
var icsBuilder = new StringBuilder();
// ICS header
icsBuilder.AppendLine("BEGIN:VCALENDAR");
icsBuilder.AppendLine("VERSION:2.0");
icsBuilder.AppendLine("PRODID:-//Org2Ics//Org2Ics//EN");
icsBuilder.AppendLine("CALSCALE:GREGORIAN");
icsBuilder.AppendLine("METHOD:PUBLISH");
// Generate events
foreach (var entry in entries)
{
GenerateEvent(icsBuilder, entry);
}
// ICS footer
icsBuilder.AppendLine("END:VCALENDAR");
File.WriteAllText(outputFile, icsBuilder.ToString(), Encoding.UTF8);
}
private void GenerateEvent(StringBuilder icsBuilder, DiaryEntry entry)
{
string uid = $"chronolog-{entry.Date:yyyy-MM-dd}@org2ics";
string dateStamp = DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ");
string eventDate = entry.Date.ToString("yyyyMMdd");
icsBuilder.AppendLine("BEGIN:VEVENT");
icsBuilder.AppendLine($"UID:{uid}");
icsBuilder.AppendLine($"DTSTAMP:{dateStamp}");
icsBuilder.AppendLine($"DTSTART;VALUE=DATE:{eventDate}");
icsBuilder.AppendLine($"DTEND;VALUE=DATE:{entry.Date.AddDays(1):yyyyMMdd}");
icsBuilder.AppendLine("SUMMARY:Chronolog");
if (!string.IsNullOrWhiteSpace(entry.Content))
{
// Escape special characters in description
string description = EscapeIcsText(entry.Content);
icsBuilder.AppendLine($"DESCRIPTION:{description}");
}
icsBuilder.AppendLine("END:VEVENT");
}
private string EscapeIcsText(string text)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
// Replace line breaks and escape special characters according to RFC 5545
return text
.Replace("\\", "\\\\")
.Replace(",", "\\,")
.Replace(";", "\\;")
.Replace("\r\n", "\\n")
.Replace("\n", "\\n")
.Replace("\r", "\\n");
}
private List<DiaryEntry> ParseIcsFile(string inputFile)
{
var entries = new List<DiaryEntry>();
var lines = File.ReadAllLines(inputFile);
bool inEvent = false;
DiaryEntry? currentEntry = null;
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i].Trim();
if (line == "BEGIN:VEVENT")
{
inEvent = true;
currentEntry = new DiaryEntry();
}
else if (line == "END:VEVENT" && inEvent && currentEntry != null)
{
if (currentEntry.Date != default &&
(string.IsNullOrEmpty(currentEntry.Content) || currentEntry.Content != "Unknown Event"))
{
entries.Add(currentEntry);
}
inEvent = false;
currentEntry = null;
}
else if (inEvent && currentEntry != null)
{
if (line.StartsWith("DTSTART;VALUE=DATE:"))
{
string dateStr = line.Substring("DTSTART;VALUE=DATE:".Length);
if (DateTime.TryParseExact(dateStr, "yyyyMMdd",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date))
{
currentEntry.Date = date;
}
}
else if (line.StartsWith("DTSTART:"))
{
// Handle datetime format YYYYMMDDTHHMMSSZ
string dateTimeStr = line.Substring("DTSTART:".Length);
if (dateTimeStr.Contains("T"))
{
string dateStr = dateTimeStr.Substring(0, 8);
if (DateTime.TryParseExact(dateStr, "yyyyMMdd",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date))
{
currentEntry.Date = date;
}
}
}
else if (line.StartsWith("DESCRIPTION:"))
{
string description = line.Substring("DESCRIPTION:".Length);
currentEntry.Content = UnescapeIcsText(description);
}
else if (line.StartsWith("SUMMARY:") && !line.Contains("Chronolog"))
{
// If summary is not "Chronolog", use it as content if no description exists
string summary = line.Substring("SUMMARY:".Length);
if (string.IsNullOrEmpty(currentEntry.Content))
{
currentEntry.Content = UnescapeIcsText(summary);
}
}
}
}
return entries.OrderBy(e => e.Date).ToList();
}
private string UnescapeIcsText(string text)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
// Unescape special characters according to RFC 5545
return text
.Replace("\\n", Environment.NewLine)
.Replace("\\;", ";")
.Replace("\\,", ",")
.Replace("\\\\", "\\");
}
private void GenerateOrgFile(List<DiaryEntry> entries, string outputFile)
{
var orgBuilder = new StringBuilder();
// Org file header
orgBuilder.AppendLine("* Diary");
orgBuilder.AppendLine();
orgBuilder.AppendLine("Converted from calendar entries.");
orgBuilder.AppendLine();
// Group entries by year and month for better organization
var groupedEntries = entries
.GroupBy(e => new { e.Date.Year, e.Date.Month })
.OrderBy(g => g.Key.Year)
.ThenBy(g => g.Key.Month);
foreach (var monthGroup in groupedEntries)
{
string monthName = new DateTime(monthGroup.Key.Year, monthGroup.Key.Month, 1)
.ToString("MMMM yyyy", CultureInfo.InvariantCulture);
orgBuilder.AppendLine($"** {monthName}");
orgBuilder.AppendLine();
foreach (var entry in monthGroup.OrderBy(e => e.Date))
{
orgBuilder.AppendLine($"*** {entry.Date:yyyy-MM-dd}");
orgBuilder.AppendLine();
if (!string.IsNullOrWhiteSpace(entry.Content))
{
orgBuilder.AppendLine(entry.Content);
}
else
{
orgBuilder.AppendLine("(No content)");
}
orgBuilder.AppendLine();
}
}
File.WriteAllText(outputFile, orgBuilder.ToString(), Encoding.UTF8);
}
}
public class DiaryEntry
{
public DateTime Date { get; set; }
public string Content { get; set; } = string.Empty;
}

134
README.md Normal file
View File

@@ -0,0 +1,134 @@
# Org2Ics - Bidirectional Org/iCalendar Converter
A C# console application that provides bidirectional conversion between .org diary files and .ics (iCalendar) format.
## Description
This tool converts between .org files containing diary entries and iCalendar (.ics) files. It supports:
- **Org to ICS**: Convert diary entries to all-day calendar events named "Chronolog"
- **ICS to Org**: Convert calendar events back to org diary format with proper date headers
## Features
- **Bidirectional conversion** between .org and .ics formats
- **Auto-detection** of input file format based on file extension
- **Org to ICS conversion**:
- Parses .org files with level 3 headings (***) containing dates in YYYY-MM-DD format
- Extracts diary content for each date entry
- Creates all-day events with the title "Chronolog"
- Includes diary content as event descriptions
- **ICS to Org conversion**:
- Parses standard .ics calendar files
- Extracts event dates and descriptions
- Organizes entries by year and month
- Creates proper org-mode structure with level 3 date headers
- **Proper character escaping** for both formats
- **Round-trip compatibility** for seamless conversion between formats
## Usage
```bash
dotnet run <input.org|input.ics> [output.ics|output.org]
```
### Parameters
- `input.org|input.ics`: Path to the input file (required)
- `.org` files will be converted to `.ics` format
- `.ics` files will be converted to `.org` format
- `output.ics|output.org`: Path to the output file (optional, auto-determined if not specified)
### Examples
```bash
# Convert diary.org to diary.ics
dotnet run diary.org
# Convert calendar.ics to calendar.org
dotnet run calendar.ics
# Specify custom output filename
dotnet run diary.org my-calendar.ics
dotnet run calendar.ics my-diary.org
# Round-trip conversion
dotnet run diary.org calendar.ics
dotnet run calendar.ics diary-restored.org
```
## File Format Support
### Org File Format (.org → .ics)
The converter expects .org files with the following structure:
```org
* My Diary
** Month Section
*** 2025-01-15
This is the diary content for January 15th, 2025.
It can span multiple lines and paragraphs.
*** 2025-01-16
Another diary entry for the next day.
** Another Section
*** 2025-02-01
February entry content here.
```
**Requirements:**
- Level 3 headings (starting with `***`) must contain dates in `YYYY-MM-DD` format
- Everything after a date heading until the next heading becomes the diary content for that date
- Non-date headings are ignored
### ICS File Format (.ics → .org)
The converter can parse standard iCalendar files and extracts:
- Event dates (both all-day and timed events)
- Event descriptions or summaries as diary content
- Events are organized chronologically and grouped by month
**Supported:**
- `DTSTART;VALUE=DATE:` (all-day events)
- `DTSTART:` (timed events - date portion used)
- `DESCRIPTION:` field content
- `SUMMARY:` field content (when no description available)
- Proper unescaping of iCalendar text formatting
## Output Formats
### Generated .ics Files (from .org)
- Standard iCalendar format (RFC 5545 compliant)
- All-day events spanning from the diary date to the next day
- Event title: "Chronolog"
- Event description: The diary content for that date
- Unique identifiers for each event
### Generated .org Files (from .ics)
- Proper org-mode structure with hierarchical headings
- Level 1: "Diary" (root heading)
- Level 2: Month and year groupings (e.g., "January 2025")
- Level 3: Individual date entries (e.g., "*** 2025-01-15")
- Content: Event descriptions properly formatted for org-mode
## Building
```bash
dotnet build
```
## Requirements
- .NET 8.0 or higher
- No additional dependencies required
## License
This project is provided as-is for educational and personal use.

View File

@@ -0,0 +1,38 @@
* Diary
Converted from calendar entries.
** January 2025
*** 2025-01-15
Today was a great day! I had a wonderful meeting with my colleagues and we discussed the new project plans. The weather was sunny and I went for a walk in the park.
I also read a good book in the evening.
*** 2025-01-16
Worked on the new features for our application. Made good progress on the user interface improvements.
Had dinner with friends at the new restaurant downtown.
** February 2025
*** 2025-02-01
Start of a new month! Set new goals for February:
- Complete the project documentation
- Exercise regularly
- Learn a new programming language
*** 2025-02-14
Valentine's Day! Spent time with family and had a lovely dinner.
*** 2025-02-28
Last day of February. Reflecting on the month's achievements:
- Successfully completed 3 major tasks
- Improved my coding skills
- Read 2 interesting books

46
samples/sample-diary.ics Normal file
View File

@@ -0,0 +1,46 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Org2Ics//Org2Ics//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:chronolog-2025-01-15@org2ics
DTSTAMP:20250830T081602Z
DTSTART;VALUE=DATE:20250115
DTEND;VALUE=DATE:20250116
SUMMARY:Chronolog
DESCRIPTION:Today was a great day! I had a wonderful meeting with my colleagues and we discussed the new project plans. The weather was sunny and I went for a walk in the park.\n\nI also read a good book in the evening.
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-01-16@org2ics
DTSTAMP:20250830T081602Z
DTSTART;VALUE=DATE:20250116
DTEND;VALUE=DATE:20250117
SUMMARY:Chronolog
DESCRIPTION:Worked on the new features for our application. Made good progress on the user interface improvements.\n\nHad dinner with friends at the new restaurant downtown.
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-02-01@org2ics
DTSTAMP:20250830T081602Z
DTSTART;VALUE=DATE:20250201
DTEND;VALUE=DATE:20250202
SUMMARY:Chronolog
DESCRIPTION:Start of a new month! Set new goals for February:\n- Complete the project documentation\n- Exercise regularly\n- Learn a new programming language
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-02-14@org2ics
DTSTAMP:20250830T081602Z
DTSTART;VALUE=DATE:20250214
DTEND;VALUE=DATE:20250215
SUMMARY:Chronolog
DESCRIPTION:Valentine's Day! Spent time with family and had a lovely dinner.
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-02-28@org2ics
DTSTAMP:20250830T081602Z
DTSTART;VALUE=DATE:20250228
DTEND;VALUE=DATE:20250301
SUMMARY:Chronolog
DESCRIPTION:Last day of February. Reflecting on the month's achievements:\n- Successfully completed 3 major tasks\n- Improved my coding skills\n- Read 2 interesting books
END:VEVENT
END:VCALENDAR

41
samples/sample-diary.org Normal file
View File

@@ -0,0 +1,41 @@
* My Diary
This is my personal diary file.
** January 2025
*** 2025-01-15
Today was a great day! I had a wonderful meeting with my colleagues and we discussed the new project plans. The weather was sunny and I went for a walk in the park.
I also read a good book in the evening.
*** 2025-01-16
Worked on the new features for our application. Made good progress on the user interface improvements.
Had dinner with friends at the new restaurant downtown.
** February 2025
*** 2025-02-01
Start of a new month! Set new goals for February:
- Complete the project documentation
- Exercise regularly
- Learn a new programming language
*** 2025-02-14
Valentine's Day! Spent time with family and had a lovely dinner.
* Some other section
This is not a diary entry and should be ignored.
*** 2025-02-28
Last day of February. Reflecting on the month's achievements:
- Successfully completed 3 major tasks
- Improved my coding skills
- Read 2 interesting books

View File

@@ -0,0 +1,24 @@
* Diary
Converted from calendar entries.
** September 2025
*** 2025-09-01
Spent the weekend relaxing at home. Watched some movies and caught up on reading.
*** 2025-09-03
First day back after the long weekend. Had several meetings and started planning the new quarter.
*** 2025-09-15
Company organized a team building event at the local park. Great fun with colleagues, lots of games and good food.
** October 2025
*** 2025-10-01
Important client meeting to discuss project requirements and timeline.

View File

@@ -0,0 +1,38 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Org2Ics//Org2Ics//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:chronolog-2025-09-01@org2ics
DTSTAMP:20250830T082255Z
DTSTART;VALUE=DATE:20250901
DTEND;VALUE=DATE:20250902
SUMMARY:Chronolog
DESCRIPTION:Spent the weekend relaxing at home. Watched some movies and caught up on reading.
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-09-03@org2ics
DTSTAMP:20250830T082255Z
DTSTART;VALUE=DATE:20250903
DTEND;VALUE=DATE:20250904
SUMMARY:Chronolog
DESCRIPTION:First day back after the long weekend. Had several meetings and started planning the new quarter.
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-09-15@org2ics
DTSTAMP:20250830T082255Z
DTSTART;VALUE=DATE:20250915
DTEND;VALUE=DATE:20250916
SUMMARY:Chronolog
DESCRIPTION:Company organized a team building event at the local park. Great fun with colleagues\, lots of games and good food.
END:VEVENT
BEGIN:VEVENT
UID:chronolog-2025-10-01@org2ics
DTSTAMP:20250830T082255Z
DTSTART;VALUE=DATE:20251001
DTEND;VALUE=DATE:20251002
SUMMARY:Chronolog
DESCRIPTION:Important client meeting to discuss project requirements and timeline.
END:VEVENT
END:VCALENDAR

38
samples/test-calendar.ics Normal file
View File

@@ -0,0 +1,38 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp//CalDAV Client//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:event1@example.com
DTSTAMP:20250830T100000Z
DTSTART;VALUE=DATE:20250901
DTEND;VALUE=DATE:20250902
SUMMARY:Labor Day Weekend
DESCRIPTION:Spent the weekend relaxing at home. Watched some movies and caught up on reading.
END:VEVENT
BEGIN:VEVENT
UID:event2@example.com
DTSTAMP:20250830T100000Z
DTSTART;VALUE=DATE:20250903
DTEND;VALUE=DATE:20250904
SUMMARY:Back to Work
DESCRIPTION:First day back after the long weekend. Had several meetings and started planning the new quarter.
END:VEVENT
BEGIN:VEVENT
UID:event3@example.com
DTSTAMP:20250830T100000Z
DTSTART;VALUE=DATE:20250915
DTEND;VALUE=DATE:20250916
SUMMARY:Team Building Event
DESCRIPTION:Company organized a team building event at the local park. Great fun with colleagues\, lots of games and good food.
END:VEVENT
BEGIN:VEVENT
UID:event4@example.com
DTSTAMP:20250830T100000Z
DTSTART:20251001T140000Z
DTEND:20251001T160000Z
SUMMARY:Meeting with Client
DESCRIPTION:Important client meeting to discuss project requirements and timeline.
END:VEVENT
END:VCALENDAR