Skip to content

Add Time Only Local and UTC Helpers - #49

Merged
jmazur merged 1 commit into
mainfrom
jm-service-day-time-fields
Sep 14, 2026
Merged

jmazur merged 1 commit into
mainfrom
jm-service-day-time-fields

Conversation

@jmazur

@jmazur jmazur commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

The previous change added _utc/_local to every date and time field
across the gem except two families that are returned as a bare
"HH:MM", no date at all. Converting one of these to a real Time
requires knowing which calendar date it belongs to, and GO Transit's
own service date doesn't line up with calendar midnight: a service
date starting at 6am can run past 2am the next calendar day, and
every stop in that overnight tail still belongs to the original
service date, not the next one.

This change addresses the need by:

  • Adding a configurable service_day_boundary_hour (default 4) marking
    where one service date ends and the next begins
  • Resolving _utc/_local for these fields by anchoring to a known
    date rather than assuming "today", correctly handling a trip whose
    very first stop is already past midnight with no other stop to
    compare it against
  • Threading the anchor date through Schedule.trip and
    Schedule.journey automatically, so no caller has to supply one
    themselves

@jmazur jmazur self-assigned this Sep 14, 2026
def to_local_from_anchor(time, trip_date, timezone: "America/Toronto")
raise MissingAnchorDateError if trip_date.nil?
return nil if time.blank?
hour = time.split(":").first.to_i

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

to_local_from_anchor(time, trip_date, timezone: timezone)&.utc
end

def to_local_from_anchor(time, trip_date, timezone: "America/Toronto")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

date.in_time_zone(timezone)
end

def to_utc_from_anchor(time, trip_date, timezone: "America/Toronto")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Comment thread lib/go_transit/version.rb
@@ -1,3 +1,3 @@
module GoTransit
VERSION = "1.2.0".freeze
VERSION = "1.3.0".freeze

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Comment thread spec/go_transit_spec.rb
expect(GoTransit.service_day_boundary_hour).to eq(4)
end

it "can be overridden via configure" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Comment thread spec/go_transit_spec.rb
@@ -0,0 +1,15 @@
RSpec.describe GoTransit do
describe ".service_day_boundary_hour" do
it "defaults to 4" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Comment thread spec/go_transit_spec.rb
@@ -0,0 +1,15 @@
RSpec.describe GoTransit do
describe ".service_day_boundary_hour" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Comment thread spec/go_transit_spec.rb
@@ -0,0 +1,15 @@
RSpec.describe GoTransit do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true.

it "cascades the anchor date to both arrival_time and departure_time" do
stop = described_class.new(
"ArrivalTime" => { "Scheduled" => "14:50", "Computed" => "", "Status" => "" },
"DepartureTime" => { "Scheduled" => "14:54", "Computed" => "", "Status" => "" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
Metrics/LineLength: Line is too long. [88/80]
Style/TrailingCommaInArguments: Avoid comma after the last parameter of a method call.

describe "#anchor_date=" do
it "cascades the anchor date to both arrival_time and departure_time" do
stop = described_class.new(
"ArrivalTime" => { "Scheduled" => "14:50", "Computed" => "", "Status" => "" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
Metrics/LineLength: Line is too long. [86/80]

@@ -0,0 +1,15 @@
RSpec.describe GoTransit::Schedule::Trip::Stop do
describe "#anchor_date=" do
it "cascades the anchor date to both arrival_time and departure_time" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

@@ -0,0 +1,15 @@
RSpec.describe GoTransit::Schedule::Trip::Stop do
describe "#anchor_date=" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

@@ -0,0 +1,15 @@
RSpec.describe GoTransit::Schedule::Trip::Stop do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true.


it "propagates its own Date to every stop, across every service and trip" do
date = Date.parse("2023-03-13")
journeys = GoTransit::Schedule.journey(date: date, from_stop_code: "UN",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end

it "propagates its own Date to every stop, across every service and trip" do
date = Date.parse("2023-03-13")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

to be_kind_of(GoTransit::Schedule::Journey::SchJourney)
end

it "propagates its own Date to every stop, across every service and trip" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

it "raises MissingAnchorDateError when anchor_date was never assigned" do
stop = described_class.new(time: "07:00")

expect { stop.time_local }.to raise_error(GoTransit::MissingAnchorDateError)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [82/80]

expect(stop.time_local.to_date).to eq(Date.new(2026, 1, 2))
end

it "raises MissingAnchorDateError when anchor_date was never assigned" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end

it "raises MissingAnchorDateError when anchor_date was never assigned" do
stop = GoTransit::Schedule::Journey::SchJourney::Service::Trip::Stop.new(time: "07:00")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [93/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end

it "rolls forward past the boundary hour, same rule as Schedule::Trip's stops" do
stop = GoTransit::Schedule::Journey::SchJourney::Service::Trip::Stop.new(time: "00:15")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [93/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

expect(stop.time_utc).to eq(stop.time_local.utc)
end

it "rolls forward past the boundary hour, same rule as Schedule::Trip's stops" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [85/80]


describe "#time_local and #time_utc" do
it "resolve using the assigned anchor_date" do
stop = GoTransit::Schedule::Journey::SchJourney::Service::Trip::Stop.new(time: "07:00")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [93/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end

describe "#time_local and #time_utc" do
it "resolve using the assigned anchor_date" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.


describe "#scheduled_utc" do
it "converts the resolved local time to UTC" do
arrival_time = GoTransit::Schedule::Trip::Stop::ArrivalTime.new(scheduled: "14:54")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [89/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end

describe "#scheduled_utc" do
it "converts the resolved local time to UTC" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end
end

describe "#scheduled_utc" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

it "raises MissingAnchorDateError when anchor_date was never assigned" do
arrival_time = GoTransit::Schedule::Trip::Stop::ArrivalTime.new(scheduled: "14:54")

expect { arrival_time.scheduled_local }.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/DotPosition: Place the . on the next line, together with the method name.

end

it "raises MissingAnchorDateError when anchor_date was never assigned" do
arrival_time = GoTransit::Schedule::Trip::Stop::ArrivalTime.new(scheduled: "14:54")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [89/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

The previous change added _utc/_local to every date and time field
across the gem except two families that are returned as a bare
"HH:MM", no date at all. Converting one of these to a real Time
requires knowing which calendar date it belongs to, and GO Transit's
own service date doesn't line up with calendar midnight: a service
date starting at 6am can run past 2am the next calendar day, and
every stop in that overnight tail still belongs to the original
service date, not the next one.

This change addresses the need by:
* Adding a configurable service_day_boundary_hour (default 4) marking
  where one service date ends and the next begins
* Resolving _utc/_local for these fields by anchoring to a known
  date rather than assuming "today", correctly handling a trip whose
  very first stop is already past midnight with no other stop to
  compare it against
* Threading the anchor date through Schedule.trip and
  Schedule.journey automatically, so no caller has to supply one
  themselves
@jmazur
jmazur force-pushed the jm-service-day-time-fields branch from b94ac48 to 06781d0 Compare September 14, 2026 23:20
expect(arrival_time.scheduled_local).to be_nil
end

it "raises MissingAnchorDateError when anchor_date was never assigned" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

end

it "returns nil when the underlying field is blank" do
arrival_time = GoTransit::Schedule::Trip::Stop::ArrivalTime.new(scheduled: "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [84/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

GoTransit.service_day_boundary_hour = original_boundary_hour
end

it "returns nil when the underlying field is blank" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

it "honors a custom service_day_boundary_hour" do
original_boundary_hour = GoTransit.service_day_boundary_hour
GoTransit.service_day_boundary_hour = 3
arrival_time = GoTransit::Schedule::Trip::Stop::ArrivalTime.new(scheduled: "03:30")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [89/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

expect(result.to_date).to eq(Date.new(2026, 1, 2))
end

it "honors a custom service_day_boundary_hour" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

RSpec.describe GoTransit::Schedule::Trip::Stop::ArrivalTime do
describe "#scheduled_local" do
it "stays on the anchor date when the hour is at or after the boundary" do
arrival_time = GoTransit::Schedule::Trip::Stop::ArrivalTime.new(scheduled: "23:58")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [89/80]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

@@ -0,0 +1,82 @@
RSpec.describe GoTransit::Schedule::Trip::Stop::ArrivalTime do
describe "#scheduled_local" do
it "stays on the anchor date when the hour is at or after the boundary" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

@@ -0,0 +1,82 @@
RSpec.describe GoTransit::Schedule::Trip::Stop::ArrivalTime do
describe "#scheduled_local" do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/BlockLength: Block has too many lines. [38/25]
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

@@ -0,0 +1,82 @@
RSpec.describe GoTransit::Schedule::Trip::Stop::ArrivalTime do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/BlockLength: Block has too many lines. [56/25]
Style/FrozenStringLiteralComment: Missing magic comment # frozen_string_literal: true.

it "raises MissingAnchorDateError" do
departure_time = GoTransit::Schedule::Trip::Stop::DepartureTime.new(scheduled: "14:54")

expect { departure_time.scheduled_local }.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/DotPosition: Place the . on the next line, together with the method name.

@jmazur
jmazur merged commit ed7f11a into main Sep 14, 2026
11 checks passed
@jmazur
jmazur deleted the jm-service-day-time-fields branch September 14, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant