One product that I have recently started using to keep myself organized is Task Coach. Task Coach is a simple open source todo manager to manage personal tasks and todo lists.

Side Note: This article was written in 2008 so some of the references may be out-of-date.

Being a work-in-progress, it is missing a number of features ( as of release 0.72.6 in April 2009 ) that I need. One is the ability to import tasks from other formats. It does however have a merge feature so you can combine tasks from other Task Coach files.

In my case, I can export events in iCalendar format from Lotus Notes at work and Google Calendar for my personal schedule. Task Coach does not yet import “*.ics” or iCalendar format files.

Instead I wrote a quick utility in Ruby to read in these files and write out a simple “*.tsk” task file – that I can then merge in.

The Utility – ical2taskcoach.rb –

Note that this version only ports over the start and due dates, the title, and the description.

It also reuses the UID (unique ID) from each iCalendar entry to populate the unique ID for each task entry – so if your input file does not contain valid unique UID values then your resulting task file will likely not merge correctly.

Usage is ical2taskcoach.rb filename

The “.ics” extension on filename is optional.

For example – ical2taskcoach golfdates will convert golfdates.ics into golfdates.tsk.

Files should be in the current working directory.

I took the lazy approach on the output file and wrote out the XML tags directly instead of using a ruby gem to generate the xml.

### 
### ical2taskcoach.rb - A simple iCal to TaskCoach conversion utility written in Ruby
### 

  require 'icalendar'
  
  $iCalfile = "";   
  $taskfile = "";
  
  if ARGV[0] != nil then $iCalfile = ARGV[0] end;
  
  if $iCalfile.size < 1 then puts "Error: No iCal file specified.Usage is: ical2taskcoach icalFile"; exit; end

  ## drop .ics extensions if entered
  
  # Build input file name
  ext = $iCalfile.upcase[-4,4];
  if ext != nil then if ext != ".ICS" then $iCalfile=$iCalfile + ".ics" end end
  
  # Build output file name
  $taskfile = $iCalfile
  ext = $iCalfile.upcase[-4,4];
  if ext != nil then if ext == ".ICS" then $taskfile=$taskfile[0,$taskfile.size-4] end end
  $taskfile = $taskfile + ".tsk"
  
  # Open a file or pass a string to the parser
  cal_file = File.open("#{$iCalfile}")

  # Parser returns an array of calendars because a single file
  # can have multiple calendars.
  cals = Icalendar.parse(cal_file)

  tcfile = File.new("#{$taskfile}", "w+"); 
  tcfile.puts "<?xml version=\"1.0\" ?>"; 
  tcfile.puts "<?taskcoach release=\"0.72.3\" tskversion=\"23\"?>"; 
  tcfile.puts "<tasks>"; 
  
    cals.each do |cal|
      cal.events.each do |mytask|
        startDT = "#{mytask.dtstart.year}-#{mytask.dtstart.month}-#{mytask.dtstart.day}"
        dueDT = "#{mytask.dtend.year}-#{mytask.dtend.month}-#{mytask.dtend.day}"
        tcfile.puts "<task " +
            "duedate=\"#{dueDT}\" " +
            "id=\"#{mytask.uid}\" " +
            "startdate=\"#{startDT}\" " +
            "status=\"1\" " +
            "subject=\"#{mytask.summary}\">"; 
        tcfile.puts "<description>#{mytask.description}</description>"; 
        tcfile.puts "</task>"; 
      end
    end
    
  tcfile.puts "</tasks>"; 
  tcfile.close;

Just run “ical2taskcoach.rb”, start up task Coach, and merge the new task file into your existing task file.

Remember, if you do not like the results of the merge then just cancel out of Task Coach without saving.

Simple enough?

The iCalendar gem –

The iCalendar gem provides an easy way to read in your iCalendar file.

<b>Installing the icalendar gem</b>
<p>If you use RubyGems, run the following and select the newest version marked "ruby" --
<p>gem install icalendar

More notes on the file format –

  tcfile.puts "&lt;?taskcoach release=\"0.72.3\" tskversion=\"23\"?&gt;"; 

This line is included so Task Coach will have some clue as to which fields are valid.

When I excluded this line completely, the description was not being imported since Task Coach was trying to maintain compatibility with an old file format where I guess description did not yet exist.

Addendum

One disadvantage to my approach to create my own XML rather than using a GEM that this version does not encode any special characters. As such, it is easy to create a file that task coach will fail to read.

Quick fix is to stick to simple text – letters, numbers, and some punctuation –on your calendar entries being converted. You can also edit the tsk file to remove or fix special characters after the fact.

I will create a “version 2” soon that fixes this issue.

Code Snippets

1. ical2taskcoach.rb

### 
### ical2taskcoach.rb - A simple iCal to TaskCoach conversion utility written in Ruby
### 
  
  require 'icalendar'
  
  $iCalfile = "";   
  $taskfile = "";
  
  if ARGV[0] != nil then $iCalfile = ARGV[0] end;
  
  if $iCalfile.size &lt; 1 then puts "Error: No iCal file specified.Usage is: ical2taskcoach icalFile"; exit; end
  
  ## drop .ics extensions if entered
    
  # Build input file name
  ext = $iCalfile.upcase[-4,4];
  if ext != nil then if ext != ".ICS" then $iCalfile=$iCalfile + ".ics" end end
  
  # Build output file name
  $taskfile = $iCalfile
  ext = $iCalfile.upcase[-4,4];
  if ext != nil then if ext == ".ICS" then $taskfile=$taskfile[0,$taskfile.size-4] end end
  $taskfile = $taskfile + ".tsk"
  
  # Open a file or pass a string to the parser
  cal_file = File.open("#{$iCalfile}")
  
  # Parser returns an array of calendars because a single file
  # can have multiple calendars.
  cals = Icalendar.parse(cal_file)
  
  tcfile = File.new("#{$taskfile}", "w+"); 
  tcfile.puts "&lt;?xml version=\"1.0\" ?&gt;"; 
  tcfile.puts "&lt;?taskcoach release=\"0.72.3\" tskversion=\"23\"?&gt;"; 
  tcfile.puts "&lt;tasks&gt;"; 
  
    cals.each do |cal|
      cal.events.each do |mytask|
        startDT = "#{mytask.dtstart.year}-#{mytask.dtstart.month}-#{mytask.dtstart.day}"
        dueDT = "#{mytask.dtend.year}-#{mytask.dtend.month}-#{mytask.dtend.day}"
        tcfile.puts "&lt;task " +
            "duedate=\"#{dueDT}\" " +
            "id=\"#{mytask.uid}\" " +
            "startdate=\"#{startDT}\" " +
            "status=\"1\" " +
            "subject=\"#{mytask.summary}\"&gt;"; 
        tcfile.puts "&lt;description&gt;#{mytask.description}&lt;/description&gt;"; 
        tcfile.puts "&lt;/task&gt;"; 
      end
    end
    
  tcfile.puts "&lt;/tasks&gt;"; 
  tcfile.close;