Skip to content

Add consistent _utc and _local date helpers - #48

Merged
jmazur merged 1 commit into
mainfrom
jm-consistent-time-helpers
Sep 13, 2026
Merged

jmazur merged 1 commit into
mainfrom
jm-consistent-time-helpers

Conversation

@jmazur

@jmazur jmazur commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner

A project I am working on assumed every time-shaped field returned a
Time-like object, because most of the gem's resource classes expose a
conversion helper (_utc) for their time fields, letting a consumer
avoid ever touching the raw value directly. A few classes had no helper
at all, leaving their raw, unconverted strings as the only way to read
the field, an easy trap for exactly the assumption that caused the bug.
Separately, no class anywhere in the gem exposed a proper local-zoned
Time either, despite every one of these fields already being local
Toronto time as returned by the API, so a consumer wanting that
representation had to parse the raw string themselves, the same class of
manual work that went wrong in the first place.

This change addresses the need by:

  • Adding a shared to_local helper to ApiResource, alongside the
    existing to_utc, both built on the same in_time_zone call
  • Adding _utc to the six classes that had no time-conversion helper at
    all (including Schedule::Line::Trip::Stop, the exact class that caused
    the my_go_api incident)
  • Adding _local to every time field across the gem, all fifteen
    classes, so every one now has both a UTC and a Toronto-zoned accessor,
    not just some of them
  • Updating the README's own claim (previously inaccurate) that every
    date and time field has a UTC conversion method
  • Leaving Schedule::Journey::SchJourney's date/time pair untouched,
    documented inline: it splits a moment across two fields rather than
    one combined string, and no real API response was available to confirm
    either field's exact format before assuming one

@jmazur jmazur self-assigned this Sep 13, 2026
to_local(date).utc
end

def to_local(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.1.0".freeze
VERSION = "1.2.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.


result = metadata.time_stamp_local

expect(result).to eq(toronto_time.in_time_zone("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.

describe "#time_stamp_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-14 12:00: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.

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


describe "#time_stamp_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") 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

describe "#time_stamp_local" do
it "returns the timestamp zoned to toronto time, unshifted" 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 "#time_stamp_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.

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


result = stop.time_local

expect(result).to eq(toronto_time.in_time_zone("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.

describe "#time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-17 12:00: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.

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

end

describe "#time_utc" do
it "converts the timestamp from toronto time to UTC 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.

end
end

describe "#time_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.


context "when is_major is not 1" do
it "is not major" do
stop = GoTransit::Schedule::Line::Trip::Stop.new(is_major: "0")

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

context "when is_major is not 1" do
it "is not major" 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

context "when is_major is not 1" 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.

context "when is_major is not 1" do
it "is not major" do
stop = GoTransit::Schedule::Journey::SchJourney::Service::Trip::Stop.new(
is_major: "0"

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.


context "when is_major is not 1" do
it "is not major" do
stop = GoTransit::Schedule::Journey::SchJourney::Service::Trip::Stop.new(

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. [81/80]

end

context "when is_major is not 1" do
it "is not major" 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

context "when is_major is not 1" 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.

context "when is_major is 1" do
it "is major" do
stop = GoTransit::Schedule::Journey::SchJourney::Service::Trip::Stop.new(
is_major: "1"

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 "#posted_date_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-17 12:00: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.

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


describe "#posted_date_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") 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

describe "#posted_date_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" 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 "#posted_date_time_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.

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


result = service.end_time_local

expect(result).to eq(toronto_time.in_time_zone("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.

@jmazur
jmazur force-pushed the jm-consistent-time-helpers branch from 648ae33 to 7199521 Compare September 13, 2026 16:21
describe "#end_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-17 12:00: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.

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


describe "#end_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") 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

describe "#end_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" 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 "#end_time_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.

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


result = service.start_time_local

expect(result).to eq(toronto_time.in_time_zone("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.

@@ -0,0 +1,45 @@
RSpec.describe GoTransit::Schedule::Line::Trip::Stop do
describe "#major?" do
context "when is_major is 1" 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,45 @@
RSpec.describe GoTransit::Schedule::Line::Trip::Stop do
describe "#major?" 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,45 @@
RSpec.describe GoTransit::Schedule::Line::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.

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


result = service.modified_date_local

expect(result).to eq(toronto_time.in_time_zone("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.

describe "#modified_date_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-17 12:00: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.

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

A project I am working on assumed every time-shaped field returned a
Time-like object, because most of the gem's resource classes expose a
conversion helper (`_utc`) for their time fields, letting a consumer
avoid ever touching the raw value directly. A few classes had no helper
at all, leaving their raw, unconverted strings as the only way to read
the field, an easy trap for exactly the assumption that caused the bug.
Separately, no class anywhere in the gem exposed a proper local-zoned
Time either, despite every one of these fields already being local
Toronto time as returned by the API, so a consumer wanting that
representation had to parse the raw string themselves, the same class of
manual work that went wrong in the first place.

This change addresses the need by:
* Adding a shared `to_local` helper to ApiResource, alongside the
  existing `to_utc`, both built on the same `in_time_zone` call
* Adding `_utc` to the six classes that had no time-conversion helper at
  all (including Schedule::Line::Trip::Stop, the exact class that caused
the my_go_api incident)
* Adding `_local` to every time field across the gem, all fifteen
  classes, so every one now has both a UTC and a Toronto-zoned accessor,
not just some of them
* Updating the README's own claim (previously inaccurate) that every
  date and time field has a UTC conversion method
* Leaving Schedule::Journey::SchJourney's date/time pair untouched,
  documented inline: it splits a moment across two fields rather than
one combined string, and no real API response was available to confirm
either field's exact format before assuming one
@jmazur
jmazur force-pushed the jm-consistent-time-helpers branch from 7199521 to 552b6a3 Compare September 13, 2026 16:24

describe "#modified_date_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") 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

describe "#modified_date_local" do
it "returns the timestamp zoned to toronto time, unshifted" 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 "#modified_date_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.

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


result = service.time_local

expect(result).to eq(toronto_time.in_time_zone("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.

describe "#time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-17 12:00: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.

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

end

describe "#update_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" 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 "#update_time_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.

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


result = service.computed_departure_time_local

expect(result).to eq(toronto_time.in_time_zone("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.

describe "#computed_departure_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") do
toronto_time = "2023-09-17 12:00: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.

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


describe "#computed_departure_time_local" do
it "returns the timestamp zoned to toronto time, unshifted" do
Timecop.travel("2023-09-14") 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.

@jmazur jmazur changed the title Jm consistent time helpers Add consistent _utc and _local date helpers Sep 13, 2026
@jmazur
jmazur merged commit fbbd23e into main Sep 13, 2026
11 checks passed
@jmazur
jmazur deleted the jm-consistent-time-helpers branch September 13, 2026 16:27
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