Skip to content

Instantly share code, notes, and snippets.

@apneadiving
Last active March 1, 2018 15:05
Show Gist options
  • Save apneadiving/6bf30407d5965943160acc267cf76ee4 to your computer and use it in GitHub Desktop.
Save apneadiving/6bf30407d5965943160acc267cf76ee4 to your computer and use it in GitHub Desktop.
Date range operations
range1 = ::Scheduling::DateRange.new(1.day.ago..Time.now)
range2 = ::Scheduling::DateRange.new(2.days.ago..Time.now)

range1.exclusion(range2) => []
range2.exclusion(range1) => [::Scheduling::DateRange.new(2.days.ago..1.day.ago)]
require "spec_helper"
describe ::Scheduling::MergeTimeslots do
def action
described_class.new(timeslots).call
end
before do
Timecop.freeze
end
after do
Timecop.return
end
let(:ref_time) { Time.now }
let(:timeslots) {[
{ starts_at: (ref_time - 3.hour).to_s, ends_at: (ref_time - 1.hour).to_s },
{ starts_at: (ref_time - 2.hour).to_s, ends_at: (ref_time).to_s },
{ starts_at: (ref_time + 2.hour).to_s, ends_at: (ref_time + 5.hour).to_s },
{ starts_at: (ref_time - 5.hour).to_s, ends_at: (ref_time + 6.hour).to_s }
].map{|ts| AdvisorshipScheduler::Timeslot.new(ts) } }
let(:expected_result) {[
{ starts_at: (ref_time - 3.hour).to_datetime.to_s, ends_at: (ref_time - 1.hour).to_datetime.to_s },
{ starts_at: (ref_time - 1.hour).to_datetime.to_s, ends_at: (ref_time).to_datetime.to_s },
{ starts_at: (ref_time + 2.hour).to_datetime.to_s, ends_at: (ref_time + 5.hour).to_datetime.to_s },
{ starts_at: (ref_time - 5.hour).to_datetime.to_s, ends_at: (ref_time - 3.hour).to_datetime.to_s },
{ starts_at: (ref_time).to_datetime.to_s, ends_at: (ref_time + 2.hour).to_datetime.to_s },
{ starts_at: (ref_time + 5.hour).to_datetime.to_s, ends_at: (ref_time + 6.hour).to_datetime.to_s }
]}
it "merges timeslots" do
expect(action).to match_array expected_result
end
end
describe ::Scheduling::MergeTimeslots::DateRangeCollection do
let(:t1) { DateTime.parse("2014-08-04 09:30") }
let(:t2) { DateTime.parse("2014-08-04 10:00") }
let(:t3) { DateTime.parse("2014-08-04 10:30") }
let(:t4) { DateTime.parse("2014-08-04 11:30") }
let(:t5) { DateTime.parse("2014-08-04 12:00") }
let(:t6) { DateTime.parse("2014-08-04 12:30") }
it "#add simple" do
collection = described_class.build([t1..t3, t4..t6])
expect(collection.ranges).to eq [build(t1..t3), build(t4..t6)]
end
it "#add larger" do
collection = described_class.build([t2..t5])
collection.add(build(t1..t6))
expect(collection.ranges).to eq [build(t1..t2), build(t2..t5), build(t5..t6)]
end
it "#add sorts" do
collection = described_class.build([t4..t6, t1..t3])
expect(collection.ranges).to eq [build(t1..t3), build(t4..t6)]
end
it "#add overlapping" do
collection = described_class.build([t1..t3, t4..t6])
collection.add(build(t2..t5))
expect(collection.ranges).to eq [build(t1..t3), build(t3..t4), build(t4..t6)]
end
it "#remove simple" do
collection = described_class.build([t1..t6])
collection.remove(build(t2..t5))
expect(collection.ranges).to eq [build(t1..t2), build(t5..t6)]
end
it "#remove overlapping" do
collection = described_class.build([t1..t3, t4..t5])
collection.remove(build(t2..t6))
expect(collection.ranges).to eq [build(t1..t2)]
end
it ".build" do
collection = described_class.new
collection
.add(build(t1..t3))
.add(build(t4..t6))
expect(collection.ranges).to eq described_class.build([t1..t3, t4..t6]).ranges
end
def build(range)
::Scheduling::MergeTimeslots::DateRange.new range
end
end
describe ::Scheduling::MergeTimeslots::DateRange do
let(:t1) { DateTime.parse("2014-08-04 09:30") }
let(:t2) { DateTime.parse("2014-08-04 10:00") }
let(:t3) { DateTime.parse("2014-08-04 10:30") }
let(:t4) { DateTime.parse("2014-08-04 11:30") }
let(:t5) { DateTime.parse("2014-08-04 12:00") }
let(:t6) { DateTime.parse("2014-08-04 12:30") }
it "union" do
r1 = build(t1..t3)
r2 = build(t2..t4)
expect(r1.union(r2)).to eq build(t1..t4)
end
it "intersection" do
r1 = build(t1..t3)
r2 = build(t2..t4)
expect(r1.intersection(r2)).to eq build(t2..t3)
end
it "exclusion - right intersection" do
r1 = build(t1..t3)
r2 = build(t2..t5)
expect(r2.exclusion(r1)).to eq [build(t3..t5)]
end
it "exclusion - left intersection" do
r1 = build(t1..t3)
r2 = build(t2..t5)
expect(r1.exclusion(r2)).to eq [build(t1..t2)]
end
it "exclusion - nothing" do
r1 = build(t1..t2)
r2 = build(t3..t4)
expect(r1.exclusion(r2)).to eq [r1]
end
it "exclusion - full" do
r1 = build(t2..t3)
r2 = build(t1..t5)
expect(r1.exclusion(r2)).to eq []
end
it "exclusion - section" do
r1 = build(t1..t4)
r2 = build(t2..t3)
expect(r1.exclusion(r2)).to eq [build(t1..t2), build(t3..t4)]
end
def build(range)
described_class.new(range)
end
end
module Scheduling
class MergeTimeslots
def initialize(timeslots)
@timeslots = timeslots
end
def call
collection.ranges.map do |elt|
{
starts_at: elt.min.to_s,
ends_at: elt.max.to_s
}
end
end
private
attr_reader :timeslots
def collection
DateRangeCollection.build timeslots.map(&:timespan)
end
class DateRangeCollection
attr_reader :ranges
def initialize
@ranges = []
end
def add(new_date_range)
to_exclude = []
ranges.each do |range|
if intersection = range.intersection(new_date_range)
to_exclude.push(intersection)
end
end
new_date_range_remainder = to_exclude.inject([new_date_range]) do |agg, timerange|
_remove(agg, timerange)
end
new_date_range_remainder.each do |elt|
@ranges.push(elt)
end
sort!
self
end
def remove(new_date_range)
@ranges = _remove(ranges, new_date_range)
self
end
def self.build(ranges)
new.tap do |collection|
ranges.each {|range| collection.add(DateRange.new(range)) }
end
end
private
def _remove(src, new_date_range)
src.each_with_object([]) do |range, array|
range.exclusion(new_date_range).each do |elt|
array.push elt
end
end
end
def sort!
ranges.sort_by! {|range| range.min }
end
end
class DateRange
attr_reader :range
def initialize(range)
@range = range
end
def intersection(other)
new_min = cover?(other.min) ? other.min : other.cover?(min) ? min : nil
new_max = cover?(other.max) ? other.max : other.cover?(max) ? max : nil
new_min && new_max ? build(new_min..new_max) : nil
end
def union(other)
new_min = cover?(other.min) ? min : other.cover?(min) ? other.min : nil
new_max = cover?(other.max) ? max : other.cover?(max) ? other.max : nil
new_min && new_max ? build(new_min..new_max) : nil
end
def exclusion(other)
if other.max < min || max < other.min
[self]
elsif (other.min <= min && other.max >= max)
[]
elsif other.min < min
[].tap do |ar|
ar.push(build((other.max)..max)) if max != other.max
end
elsif other.max > max
[].tap do |ar|
ar.push(build(min..(other.min))) if min != other.min
end
else
[].tap do |ar|
ar.push(build(min..(other.min))) if min != other.min
ar.push(build((other.max)..max)) if max != other.max
end
end
end
delegate :min, :max, :cover?, to: :range
def ==(other)
range == other.range
end
private
def build(range)
self.class.new(range)
end
end
end
end
@axhamre
Copy link

axhamre commented Mar 1, 2018

This looks purely amazing! :D

One thing though, I had to replace range1 = ::Scheduling::DateRange.new(1.day.ago..Time.now).

with range1 = ::Scheduling::MergeTimeslots::DateRange.new(1.day.ago..Time.now)

Thank you a billion times for this code!! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment