Motivation
ActiveSupport::Duration#parts returns all built in units (years, months, days, etc.) without folding larger units into smaller ones. While often this is what we need, I found myself repeatedly in a situation where I wanted only specific units and larger units to be converted down into them.
For example:
(1.day + 1.hour + 10.minutes).in_specific_parts(:hours, :minutes)
# => { hours: 25, minutes: 10 }
This is useful for reporting and simplifying time calculations where parts is too granular.
Behavior
- Accepts one or more units from
ActiveSupport::Duration::PARTS - Converts larger units into smaller ones using
PARTS_IN_SECONDS - Drops any leftover smaller than the smallest requested unit
- Preserves the sign for negative durations
Proposed Implementation
This is how I am currently extending ActiveSupport::Duration in my project.
def in_specific_parts(*requested_parts)
raise ArgumentError, "You must specify at least one component" if requested_parts.empty?
requested_parts = requested_parts.flatten.map(&:to_sym)
# Validate units
valid_parts = ActiveSupport::Duration::PARTS
unknown_parts = requested_parts - valid_parts
raise ArgumentError, "Unknown components: #{unknown_parts.join(', ')}" if unknown_parts.any?
sign = to_i.negative? ? -1 : 1
remaining_seconds = to_i.abs
# Sort largest → smallest
sorted_parts = requested_parts.sort_by { |u| -ActiveSupport::Duration::PARTS_IN_SECONDS[u] }
output_parts = {}
sorted_parts.each do |unit|
part_value, remaining_seconds = remaining_seconds.divmod(ActiveSupport::Duration::PARTS_IN_SECONDS[unit])
output_parts[unit] = part_value * sign
end
output_parts
end
Discussion Points
- Would this be broadly useful enough to warrant inclusion in core Rails?
- Any naming alternatives that feel more Rails-like?
- Any improvements on the method itself that you identify?
If there’s interest, I’m happy to submit a PR with tests and documentation (Would be my first open source contribution, so I would probably need a bit of guidance though
)