class Time

def change(options)

Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, hour: 0) # => Time.new(1981, 8, 29, 0, 0, 0)
Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)

:offset. Pass either :usec or :nsec, not both.
:hour, :min, :sec, :usec, :nsec,
takes a hash with any of these keys: :year, :month, :day,
and minute is passed, then sec, usec and nsec is set to 0. The +options+ parameter
the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour
:sec, :usec, :nsec) reset cascadingly, so if only
to the +options+ parameter. The time options (:hour, :min,
Returns a new Time where one or more of the elements have been changed according
def change(options)
  new_year   = options.fetch(:year, year)
  new_month  = options.fetch(:month, month)
  new_day    = options.fetch(:day, day)
  new_hour   = options.fetch(:hour, hour)
  new_min    = options.fetch(:min, options[:hour] ? 0 : min)
  new_sec    = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
  new_offset = options.fetch(:offset, nil)
  if new_nsec = options[:nsec]
    raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
    new_usec = Rational(new_nsec, 1000)
  else
    new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
  end
  raise ArgumentError, "argument out of range" if new_usec >= 1000000
  new_sec += Rational(new_usec, 1000000)
  if new_offset
    ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, new_offset)
  elsif utc?
    ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec)
  elsif zone
    ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec)
  else
    ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, utc_offset)
  end
end