I'm speaking about the "RiCal experience" tomorrow evening 20 October 2009, at the October meeting of the Raleigh Ruby Brigade. As usual the meeting is at Red Hat HQ in Raleigh and starts at 7:00 pm.
I plan to talk about calendars, time, time zones, camels, icalendar, Ruby implementation of complex "stuff", among other things.
I hope to see you there!
Yesterday, David released RSpec 1.2.7, which includes a patch I provided to allow the specification of where to find the 'ruby' program when creating a SpecTask, rather than relying Rakes RUBY variable.
Why did I submit this patch you ask, assuming you didn't read the title of this post?
So you can do this in a Rakefile :
multiruby_path = `which multiruby`.chomp if multiruby_path.length > 0 && Spec::Rake::SpecTask.instance_methods.include?("ruby_cmd") namespace :spec do desc "Run all specs with multiruby and ActiveSupport" Spec::Rake::SpecTask.new(:multi) do |t| t.spec_opts = ['--options', "spec/spec.opts"] t.spec_files = FileList['spec/**/*_spec.rb'] t.ruby_cmd = multiruby_path end end end
This is derived from something I just added to RiCal but haven't yet released.
What it does is check that you have multiruby, which is part of the zentest gem, installed, and that your version of RSpec supports the new ruby_cmd option. If both conditions are met it makes a spec task which runs the specs using multiruby instead of ruby.
Now it's easy to run specs with the various ruby versions you want to support.
Yesterday I took a few hours and attended an intro to SolidWorks at TechShop Durham. Then today Kent tweeted about how JUnit Max has been faring, and it got me thinking about the state of the business in software for programmers.
For those of you unfamiliar with SolidWorks, it's a 3-D Parametric Modeling program, one of the newer and most popular CAD programs. If you are a fan of the TV Show American Chopper it's the program which Jason Pohl uses to design the bikes. It has been gradually taking over the 3D mechanical CAD market, replacing old leaders like AutoCad and ProEngineer. My main interest is as a hobbyist. I've been very interested in industrial technology since I was a kid, and I like to build scale models. I'd love to have SolidWorks to play with on my own, but it's out of my price range.
SolidWorks is owned by a big corporation Dassault Systems, and it's sold through a network of VARs. Yesterdays session was given by a support engineer from the local VAR. I haven't seen an 'official' price for a license, I think they get negotiated by businesses, but I understand that the cost of a single SolidWorks 'seat' is $5,000 to $10,000 with a yearly maintenance fee of around $1000 which gets you yearly upgrades and other goodies. There's also an 'educational' license which is carefully controlled.
And that price seems well worth it to a lot of companies.
This reminds me of the way the software development tools business used to be. IBM and others got big bucks for development seat licenses for compilers, ides and other tools, sold by salesmen and supported by customer support engineers. One of the reasons Smalltalk had a hard time gaining a foothold was that ObjectWorks and VisualAge licenses were very expensive, even Digitalk Smalltalk which was targeted at the same market as Borland TurboPascal cost a few hundred bucks.
Imagine there was a time when folks actually PAID for compilers for PCs!
Nowadays that model is harder and harder to support. Most of the tools programmers use, at least the programmers I hang with, are open source, and freely available. Every once in a while a program like TextMate will pry €39 or so from a programmer's pocket, but most of us get by living off the fat of the open source land.
Unlike the proverbial shoe maker's children, it's not that we don't have any shoes, we've got all the free shoes we want.
JUnit Max, as I understand it, is an Eclipse plugin which acts as a Java equivalent to Ruby's autotest, except on steroids. While autotest runs the most recently failing test until it passes then runs all of the tests, JUnit Max queues up retests based on how recently they failed. Knowing Kent as I do, I'm sure that it's a great tool, very well crafted, but I'm just not into Java anymore, so I don't have direct experience.
Kent is asking a measly $2 per month for JUnit (at least while it's in its beta version), but judging from his tweet this morning, he's disappointed at the number of takers so far.
I admit it, we programmers tend to be a miserly lot, and a lot of us end up doing a lot of code for the love of it. We might ask for a small fee, or put up a "tip-jar" like the one on the RiCal github page but it doesn't often amount to much monetary reward.
Boy, I wish I could come up with the next SolidWorks!
The other day, someone brought up a UTF-8 related issue with RiCal.
RFC2445 specifies that each line of a icalendar datastream must be no more than 75 bytes, and longer lines need to be folded by breaking them into sections with the second and following sections put into lines with an initial space character to mark them as continuation lines. As was pointed out to me, simply breaking a UTF-8 string in Ruby runs the risk of splitting up a multi-byte character.
Here's a spec to show what I needed:
describe "String#safe_utf8_split" do
context "For an all-ascii string" do
before(:each) do
@it = "abcdef"
end
it "should properly split an ascii string when n leaves 1 character" do
@it.utf8_safe_split(5).should == ["abcde", "f"]
end
it "should return a nil remainder if the string has less than n characters" do
@it.utf8_safe_split(7).should == ["abcdef", nil]
end
it "should return a nil remainder if the string has exactly n characters" do
@it.utf8_safe_split(6).should == ["abcdef", nil]
end
end
context "For a string containing a 2-byte UTF-8 character" do
before(:each) do
@it = "Café"
end
it "should split properly just before the 2-byte character" do
@it.utf8_safe_split(3).should == ["Caf", "é"]
end
it "should split before when n is at the start of the 2-byte character" do
@it.utf8_safe_split(4).should == ["Caf", "é"]
end
it "should split after when n is at the second byte of a 2-byte character" do
@it.utf8_safe_split(5).should == ["Café", nil]
end
end
context "For a string containing a 3-byte UTF-8 character" do
before(:each) do
@it = "Prix €200"
end
it "should split properly just before the 3-byte character" do
@it.utf8_safe_split(5).should == ["Prix ", "€200"]
end
it "should split before when n is at the start of the 3-byte character" do
@it.utf8_safe_split(6).should == ["Prix ", "€200"]
end
it "should split before when n is at the second byte of a 3-byte character" do
@it.utf8_safe_split(7).should == ["Prix ", "€200"]
end
it "should split after when n is at the third byte of a 3-byte character" do
@it.utf8_safe_split(8).should == ["Prix €", "200"]
end
end
end
So to fix this I came up with a pretty simple idea, split the string and check to see if the second part is valid UTF-8:
class String
def valid_utf8?
unpack("U") rescue nil
end
def utf8_safe_split(n)
if length <= n
[self, nil]
else
before = self[0, n]
after = self[n..-1]
until after.valid_utf8?
n = n - 1
before = self[0, n]
after = self[n..-1]
end
[before, after.empty? ? nil : after]
end
end
endIn RiCal, I actually implemented this using functional methods in another object, since I didn't want to 'pollute' Strings instance methods, but the code here illustrates the basic idea.
After a few weeks of maturation on github, setting up a bug tracker, and a google group for project discussions, the bug reports died down to the point where I felt comfortable putting a more "official" release out on rubyforge.
Thanks to my most active 'beta-testers', Adam Williams who really drove the calendar generation DSL, and Paul Scott-Murphy, and Bruno Duyé gave a much needed workout to occurrence enumeration.
With folks from Australia and France providing input, it felt a bit like the old OTI days
I pushed version 0.0.6 of RiCal to GitHub. So far it seems to be getting a fair bit of interest. To-date 52 github users are following the project, and 7 have forked it, several of whom have send pull requests for patches and or additional specs when they discover bugs.
I should also mention that I've set up a google group for discussing the library. Anyone can join, but I've got it set so that I need to moderate a new users initial posting.
Run>Code>Run is also keeping an watchful eye and doing continuous integration testing of pushes to github. So far once Run>Code>Run installed the tzinfo gem all the commits have successfully tested.
And finally, Mirko Stocker, who writes for InfoQ, contacted me shortly after I made my low-key announcement of RiCal, and asked me if I would do an email interview. You can see the results of that request here.
Well, "a day or two" turned out to be a little more like a week, and the official release is still a bit in the future, but I've just published an unofficial version of ri_cal on github.
I've been working with Adam who is using github for a Rails app. He's been feeding me bugs to fix and features to add. Most of the work in the past week has been on developing the "DSL" for creating calendars and events programatically. There are a few loose ends to tie up but if you are adventurous, you might want to play with the library. There will likely be some api changes before the initial release.
When I'm ready to make the "official" release, I'll publish it to RubyForge as well as to git hub, and the version will be bumped to 0.x.0 once I've decided what x should be.
RiCal represents a considerable effort over the past several months. I hope someone besides Adam finds it useful.
I just wrote about the imminent release of my new gem rical which is a fresh implementation of the RFC 2445 (iCalendar) specification for Ruby.
Almost as an afterthought, I decided to look at what it would take to make the gem work with Ruby 1.8.6, 1.8.7 and 1.9.1. I hadn’t really thought about 1.9 compatibility while working on rical, so I didn’t know how much work would be needed.
I’d seen that David Chelimsky had said that a new version of RSpec which ran on Ruby 1.9 was imminent but that was over a month ago and I hadn’t heard an update, but David responded quickly to my email (using his iPhone while stopped at a traffic light!) that yes that rspec 1.2.4 does indeed work with 1.9, and I was off to the races.
Executive Summary
It turns out that making rical compatible with Ruby 1.9 was fairly easy. The problems fell into a few areas: * Ruby 1.9 has made DateTime parsing less flexible. * Ruby 1.9 Range#include? requires the range to be iterable * Hash enumeration order is different
Multiruby
The first step was to set up multiruby. I’d used multiruby before but not in a while. Actually I had just looked at it again, because the last few alpha drops of Maglev have required a specific patchlevel of Ruby 1.8.6 and ParseTree. Maglev currently relies on ParseTree to turn Ruby source code into an s-expression which it then turns into objects and extended Gemstone Smalltalk byte codes. Eventually this will be replaced by a native compiler of some sort.
I figured that the easiest way to get this version was via multiruby, but I was having troubles getting Maglev to work with it. It couldn’t find the Parsetree gem. It turned out that I had a back level of ZenTest (which contains multiruby along with autotest and a lot of other useful stuff). But I got tied up most of yesterday with the Radiant sprint, so I didn’t get things straightened out until I got home yesterday evening.
I updated zentest, blew away the .multiruby directory in my home directory, and reinstalled the ‘usual’ and ruby gems
multiruby_setup the_usual multiruby_setup update:rubygems
Now I could start to try to run my specs under 1.8.6, 1.8.7, and 1.9.
multiruby -S rake specThis led to a series of missing gems, first rspec. Multiruby by default has a gem directory for each version of ruby which is separate from your normal gem directory. It’s possible to share these, but going with the path of least resistance, I just installed each missing gem as I discovered it. For example to install rspec for all the multiruby installed versions:
multiruby -S gem install rspecI used the bones gem to setup the rical gem. When I installed this gem I ran into a hitch. Although the rical gem doesn’t need it for deployment, the bones gem (like many of it’s other brother gem developer assistants) adds rake tasks which depend on other gems. Since the bones provided rakefile defined a spec:rcov task which requires the rcov gem, which doesn’t seem to be 1.9 compatible yet, I got to a point where although the specs would run on 1.8.6 and 1.8.7, the rakefile (or maybe it was a task file) was preventing rake from initializing under 1.9.
At this point I took advantage of the separate gem directories under the .multiruby directory structure and hacked the copy of the bones gem under the 1.9 install to remove the need for rcov, since I was really only interested in whether the specs ran on 1.9.
The Issues
As I said, I didn’t know what to expect in terms of compatibility. The spec suite for rical has 550 spec examples.
As it turns out, although there were lots of failures, they fell into large clumps.
DateTime.parse differences
In Ruby 1.8 the DateTime.parse method uses heuristics which attempt to figure out whether certain strings represent dates in different local formats. For example, here in the US a date like “12/8/2001” is interpreted as December 8, 2001, while in most of Europe it would be August 12, 2001. I’ve gotten in the personal habit of writing such a date as 8 December 2001, which uses the European order, but is also understandable to my countrymen.
I wrote many spec examples directly from the recurrence rule use cases in the RFC2445 spec which tend to use American style dates in the document, mm/dd/yyyy. The Ruby 1.8 datetime seems to get these right.
Ruby 1.9 dropped this and always interprets nn/nn/nnnn as dd/mm/yyyy, which broke LOTs of my spec examples.
Luckily the breakage was all in the specs, not in the rical code under test. So a bit of regex replacement magic with Textmate to reformat the test input data, and lots of spec examples now ran under 1.9 as well as 1.8.x.
Range#include? Difference
In the recurrence rule enumeration code I had a method:
def in_outer_cycle?(candidate)
candidate && (outer_range.nil? || outer_range.include?(candidate))
endThe outer_range attribute is a Range of a starting and ending DateTime.
In Ruby 1.8.x Range#include? checks to see if the argument is >= the start of the range, and either <= or < the end of the range if the range is inclusive or exclusive.
In Ruby 1.9, was getting an error like “can’t iterate from DateTime.” Range#include? now actually works more like Array#include? and steps through the elements of the range.
The fix in this case was to change the method to:
def in_outer_cycle?(candidate)
candidate && (outer_range.nil? || (outer_range.first <= candidate && outer_range.last >= candidate))
endHash enumeration order difference
In Ruby 1.9 hashes keep track of the order of key insertion, and when a hash is enumerated it yields the keys or values or key value pairs in that order.
In Ruby 1.8 the enumeration order is accidental and depends on the key hash values and whether or not there are key collisions.
My last problem was a spec which read:
it "should properly format dtstart with a date-time with a local time zone" do
@it.dtstart = date_time_with_tzinfo_zone(DateTime.parse("4/22/2009 17:55"), "America/New_York")
@it.export.should match(/^DTSTART;TZID=America\/New_York;X-RICAL-TZSOURCE=TZINFO;VALUE=DATE-TIME:20090422T175500$/)One thing that I’d already change was the string argument to DateTime.parse. But that wasn’t enough.
The match expectation was failing under 1.9 because the substrings, “;TZID=America/NewYork”, “;X-RICAL-TZSOURCE=TZINFO”, and “;VALUE=DATE-TIME” were appearing in a different order under Ruby 1.9 than 1.8.x.
This was coming from a method:
# Return a string representing the receiver in RFC 2445 format
def to_s
if visible_params && !visible_params.empty?
"#{visible_params.map {|key, val| ";#{key}=#{val}"}}:#{value}"
else
":#{value}"
end
endThe order of those sub-pieces depends on the enumeration order of visible_params which is a hash.
I left this last problem overnight, to sleep on.
This example is over-specified. The order of those three substrings doesn’t really matter. Now I could write a somewhat more complicated spec example, but pragmatically I decided to leave it over-specified but specify an order which I could guarantee under all ruby versions. So the to_s method became:
# Return a string representing the receiver in RFC 2445 format
def to_s
# We only sort for testability reasons
if (vp = visible_params) && !vp.empty?
"#{vp.keys.sort.map {|key| ";#{key}=#{vp[key]}"}.join}:#{value}"
else
":#{value}"
end
endAnd the example changed slightly to:
it "should properly format dtstart with a local time zone" do
@it.dtstart = date_time_with_tzinfo_zone(DateTime.parse("April 22, 2009 17:55"), "America/New_York")
@it.export.should match(/^DTSTART;TZID=America\/New_York;VALUE=DATE-TIME;X-RICAL-TZSOURCE=TZINFO:20090422T175500$/)
endNote the change to the Ruby 1.9 friendly “April 22, 2009…” and the re-arrangement of the substrings in the regular expression.
So after I got the multiruby setup straight, it really only took an hour or two to make rical 1.9 compatible.
A few months back I wrote about my quest for a new iCalendar library for Ruby. I’ve continued work on it, and it should be ready for its initial public “offering” in a day or two.
I’ve applied for a Rubyforge project called rical (for Ruby iCALendar, although I’d prefer that it be pronounced RICK AL), other than that, I’m waiting for Adam Wiliams who has been using a private pre-release copy and has been pestering me to release it for some weeks now gives it another “smoke test” and some additional rspec examples if he finds any smoke.
I’ve made progress on a couple of fronts since the last report.
Although occurrence enumeration was reasonably fast there were a few use cases which were noticeably slower. Unfortunately these tended to be those heavily used in the recurrence rules typically used in time zones, ones which occurred just a few times in a large period, like every second Sunday in March at 1:00 A.M. I’ll probably write about why, and the odyssey of re-factoring which ensued in a later post, but suffice it to say, I’ve got a brand new design and implementation of recurrence rule enumeration which is significantly faster. The old implementation ran the entire spec suite, including all of the use cases in RFC 2445 in 15+ seconds on my late 2008 white MacBook, the new implementation runs a slightly larger suite in less than 5 seconds.
Adam hasn’t been using enumeration (yet, I hope he will soon though). His motivation was solving problems first in parsing icalendar input from other calendar apps using the existing Ruby iCalendar libraries. The VTIMEZONE support in rical is a big help here. He found a few deviations from RFC2445 which rical used to share with the icalendar and vpim gems, but doesn’t anymore.
Adam’s next need was proper exporting of RFC2445 output, which until then was lightly implemented. He also prodded me for a builder DSL for creating calendars and calendar components similar to that in the existing iCalendar gems. He offered to work on that this weekend while I focused on exporting. But two things changed the plan. I got the exporting done rather quickly, which was lucky because his wife’s OB/GYN apparently decided that she needed to deliver a new son via caesarian right now! So I went ahead and did the builder DSL myself.
Since the dead tree version of 3rd edition of the Pickaxe showed up in my mailbox on Friday, I decided that it might be good to work on making the library Ruby 1.9 compatible. So I started on that Friday night, and with lots of interruptions, including spending most of yesterday at a Radiant sprint, just finished a few minutes ago. Rical runs fine on Ruby 1.8.6, 1.8.7 and 1.9.1
So barring any surprises from Adam’s explorations, rical should be published soon after I get approval from Rubyforge for the project. I’m also planning to push it to a new github repository under my github account to keep the ‘edge’ version.
I’ve tried to keep rical ‘pure ruby’ and not Rails specific, although it should play well with Rails. In particular it will pick up timezones from ActiveSupport enhanced Ruby Time and DateTime objects using the TimeWithZone stuff which arrived in Rails 2.2. After things settle down a bit, I’m thinking about starting a Rails plugin which will exploit rical and ActiveRecord to help store calendar components in the database.
Although it would have been nice to have more billable time which would have meant that rical might not be quite as ready to ship, I’m looking forward to giving it more exposure, so far I’m pretty happy with it and I hope that others will be as well. Adam seems to think some will.
Watch this space.
Joe Van Dyk, a reader, posted a question in response to my recent article about my work towards a new gem providing iCalendar support for Ruby
How do people represent recurring events in a database? I’ve never been able to come up with a good way.
My particular problem is that each event that reoccurs needs to be able to be worked with individually (i.e. comments added to it), deleted, moved, etc.
Rather than answering directly in the comments section, I'll give my thoughts on this as a new article
My approach to this would start with recognizing the difference between an event, and an occurrence of that event. Each event would be stored as a single entity on the database, no matter how many occurrences that event had. Each event entity would have a start date-time which would be the start time of the first occurrence, and a last occurence end date-time which would be the end of the last occurrence if the event had a defined last-occurrence, or null if the recurrence set was open ended.
A non-recurring event would be handled as a degenerate case of a recurring event, with a single occurrence.
The idea is to allow for a database query which returns the set of (recurring) events which have any occurrences between two, points in time. The use-case here is displaying a calendar page, where I need to find all of the events for a given, day, week, month, etc.<\p>
Once I had those events, I'd ask each one for a list of occurrences within the target range, which for some events and some ranges might be empty.
Individual Occurrences
As Joe points out there are time when you need to talk about individual occurrences of an event. RFC 2445 talks about this. There are basic mechanisms in iCalendar to support use-cases like editing a series of recurring events. So iCalendar, defines three attributes which combine to connote a particular event, set of occurrences, or a particular occurrence
- UID
- A globally unique identifier which identifies a particular event with all of it's occurrences. The set of occurrences may change if the event is edited, with different edits being identified by the ...
- Sequence Number
- The revision sequence number of a calendar component, in this case a VEVENT, with a given UID
- Recurrence-ID
- Which represents either a particular occurrence of an event identified by a UID and sequence number, or subsequence either at the beginning or end of the list of occurrences for an event. The value of the recurrence id is a string containing either:
- A string representation of the start time and date for the occurrence or
- A string representation of the date of the occurrence in the case of an anniversary (all-day) event or
- Either of the first two, preceded by "THISANDFUTURE:", indicating all occurrences starting with the one identified by the date or date-time or
- Either of the first two, preceded by "THISANDPRIOR:", indicating all occurrences starting with the first through the one identified by the date or date-time or
I'd put any attributes associated with an individual occurrence into a separate entity belonging to the event, which would have many of these, and identified with a recurence-id.
Just a word of caution, here. This article reflects a mixture of things I have actually done in the past, with some thought experimentation. I haven't actually had to deal with attributes for occurences, but what I've described 'feels right' to me as a first approximation.




