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)]
Last active
March 1, 2018 15:05
-
-
Save apneadiving/6bf30407d5965943160acc267cf76ee4 to your computer and use it in GitHub Desktop.
Date range operations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!! ❤️