Here's my solution. Like others, it uses Newton's method. It may have
problems because it always uses zero as its initial estimate, though
all the test data I've thrown at it seems to work properly. The one
thing that is different than the other solutions I've seen posted is
that I wrapped everything up in a class. This is also my first quiz.

class IncomeStream
  DEFAULT_TOLERANCE = 0.000_000_05

  def self.irr(*flows)
    self.new(flows).irr
  end

  def self.npv(discount_rate, *flows)
    self.new(flows).npv(discount_rate)
  end

  def self.dpv(amount, time)
    lambda {|rate| (-time) * amount * (1+rate)**(-time-1)}
  end

  def self.pv(amount, time)
    lambda {|rate| amount * (1+rate)**(-time)}
  end

  def initialize(flows)
    @flows = flows.to_a
    if (not @flows.empty?) and (@flows.first.kind_of? Numeric)
      @flows.each_index {|idx| @flows[idx]=[@flows[idx],idx]}
    end
    @pvs = @flows.map {|amount, time| IncomeStream.pv(amount, time)}
    @dpvs = @flows.map {|amount, time| IncomeStream.dpv(amount, time)}
  end

  def npv(rate)
    @pvs.inject(0) {|npv, item| npv+item.call(rate)}
  end

  def dnpv(rate)
    @dpvs.inject(0) {|dnpv, item| dnpv+item.call(rate)}
  end

  def irr(tolerance=DEFAULT_TOLERANCE)
    return nil if @flows.empty? or @flows.all? {|amount, time| amount = 0}
    rate = 0.0
    while (((n=npv(rate)).abs > tolerance) and rate.finite?)
      d = dnpv(rate)
      rate -= n/d
    end
    rate
  end