Skip to content

Commit 40fb105

Browse files
committed
fix(office365): default view interval for get availability
1 parent ebd1a1d commit 40fb105

File tree

4 files changed

+114
-2
lines changed

4 files changed

+114
-2
lines changed

shard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: place_calendar
2-
version: 4.29.1
2+
version: 4.29.2
33

44
crystal: ">= 0.36.1"
55

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
require "spec"
2+
3+
module PlaceCalendar
4+
describe Office365Availability do
5+
describe ".select_view_interval" do
6+
it "returns 5 minutes for small windows" do
7+
Office365Availability.select_view_interval(15.minutes)
8+
.should eq(5)
9+
10+
Office365Availability.select_view_interval(29.minutes)
11+
.should eq(5)
12+
end
13+
14+
it "handles exact boundaries safely" do
15+
Office365Availability.select_view_interval(6.minutes)
16+
.should eq(5)
17+
18+
Office365Availability.select_view_interval(10.minutes)
19+
.should eq(5)
20+
end
21+
22+
it "scales up for hour-scale windows" do
23+
Office365Availability.select_view_interval(2.hours)
24+
.should eq(5)
25+
26+
Office365Availability.select_view_interval(8.hours)
27+
.should eq(15)
28+
end
29+
30+
it "returns sensible intervals for day-scale windows" do
31+
Office365Availability.select_view_interval(24.hours)
32+
.should eq(30)
33+
34+
Office365Availability.select_view_interval(7.days)
35+
.should eq(6.hours.total_minutes)
36+
end
37+
38+
it "returns daily buckets for very large windows" do
39+
Office365Availability.select_view_interval(30.days)
40+
.should eq(1.day.total_minutes)
41+
end
42+
43+
it "raises for windows too small to satisfy Graph constraints" do
44+
expect_raises(ArgumentError) do
45+
Office365Availability.select_view_interval(5.minutes)
46+
end
47+
48+
expect_raises(ArgumentError) do
49+
Office365Availability.select_view_interval(3.minutes)
50+
end
51+
end
52+
53+
it "always returns an interval strictly smaller than the window" do
54+
windows = [
55+
6.minutes,
56+
20.minutes,
57+
90.minutes,
58+
12.hours,
59+
3.days,
60+
]
61+
62+
windows.each do |w|
63+
interval = Office365Availability.select_view_interval(w)
64+
interval.minutes.should be < w
65+
end
66+
end
67+
end
68+
end
69+
end

src/office365.cr

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "office365"
2+
require "./office365_availability"
23

34
module PlaceCalendar
45
class Office365 < Interface
@@ -426,7 +427,7 @@ module PlaceCalendar
426427
end
427428

428429
def get_availability(user_id : String, calendars : Array(String), starts_at : Time, ends_at : Time, **options) : Array(AvailabilitySchedule)
429-
view_interval = options[:view_interval]? || 30
430+
view_interval = options[:view_interval]? || Office365Availability.select_view_interval(starts_at - ends_at)
430431

431432
# Max is 100 so we need to batch if we're above this
432433
if calendars.size > 100

src/office365_availability.cr

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module PlaceCalendar::Office365Availability
2+
# "Nice" bucket sizes in minutes (descending for easy selection)
3+
NICE_INTERVALS_MINUTES = [
4+
1440, 720, 480, 360, 240, 180,
5+
120, 60, 30, 20, 15, 10, 5,
6+
]
7+
8+
MIN_INTERVAL = 5
9+
MAX_INTERVAL = 1440
10+
TARGET_SLOTS = 24
11+
12+
def self.select_view_interval(window : Time::Span) : Int32
13+
# Convert to minutes, rounding up to avoid zero-length edge cases
14+
window_minutes = (window.total_seconds / 60.0).ceil.to_i
15+
16+
# Graph constraint: interval must be >= 5 and < window
17+
if window_minutes <= MIN_INTERVAL
18+
raise ArgumentError.new(
19+
"Window must be greater than #{MIN_INTERVAL} minutes (got #{window_minutes})"
20+
)
21+
end
22+
23+
# Target an interval that yields ~TARGET_SLOTS buckets
24+
raw = ((window_minutes - 1) // TARGET_SLOTS)
25+
26+
# Pick the largest "nice" interval <= raw
27+
interval_minutes =
28+
NICE_INTERVALS_MINUTES.find { |m| m <= raw } || MIN_INTERVAL
29+
30+
# Safety: ensure strict < window
31+
while interval_minutes >= window_minutes
32+
idx = NICE_INTERVALS_MINUTES.index(interval_minutes)
33+
interval_minutes =
34+
idx && idx < NICE_INTERVALS_MINUTES.size - 1 ? NICE_INTERVALS_MINUTES[idx + 1] : MIN_INTERVAL
35+
end
36+
37+
# Clamp (defensive, should never trigger)
38+
interval_minutes = interval_minutes.clamp(MIN_INTERVAL, MAX_INTERVAL)
39+
40+
interval_minutes
41+
end
42+
end

0 commit comments

Comments
 (0)