mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Add chronic_duration in our code to customize it
This commit is contained in:
@@ -230,8 +230,6 @@ gem "turbo-rails", "~> 2.0.0"
|
||||
|
||||
gem "httpx"
|
||||
|
||||
gem "gitlab_chronic_duration"
|
||||
|
||||
group :test do
|
||||
gem "launchy", "~> 3.0.0"
|
||||
gem "rack-test", "~> 2.1.0"
|
||||
|
||||
@@ -556,8 +556,6 @@ GEM
|
||||
fuubar (2.5.1)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
gitlab_chronic_duration (0.12.0)
|
||||
numerizer (~> 0.2)
|
||||
glob (0.4.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
@@ -749,7 +747,6 @@ GEM
|
||||
nokogiri (1.16.5)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
numerizer (0.2.0)
|
||||
oj (3.16.3)
|
||||
bigdecimal (>= 3.0)
|
||||
okcomputer (1.18.5)
|
||||
@@ -1214,7 +1211,6 @@ DEPENDENCIES
|
||||
fog-aws
|
||||
friendly_id (~> 5.5.0)
|
||||
fuubar (~> 2.5.0)
|
||||
gitlab_chronic_duration
|
||||
gon (~> 6.4.0)
|
||||
good_job (= 3.26.2)
|
||||
google-apis-gmail_v1
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
# Copied from https://gitlab.com/gitlab-org/ruby/gems/gitlab-chronic-duration
|
||||
# version 0.12.0
|
||||
#
|
||||
# Copyright (c) Henry Poydar
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without
|
||||
# restriction, including without limitation the rights to use,
|
||||
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following
|
||||
# conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# NOTE:
|
||||
# Changes to this file should be kept in sync with
|
||||
# frontend/src/app/shared/helpers/chronic_duration.js.
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
module ChronicDuration
|
||||
extend self
|
||||
|
||||
class DurationParseError < StandardError
|
||||
end
|
||||
|
||||
# On average, there's a little over 4 weeks in month.
|
||||
FULL_WEEKS_PER_MONTH = 4
|
||||
|
||||
@@raise_exceptions = false
|
||||
@@hours_per_day = 24
|
||||
@@days_per_month = 30
|
||||
|
||||
def self.raise_exceptions
|
||||
!!@@raise_exceptions
|
||||
end
|
||||
|
||||
def self.raise_exceptions=(value)
|
||||
@@raise_exceptions = !!value
|
||||
end
|
||||
|
||||
def self.hours_per_day
|
||||
@@hours_per_day
|
||||
end
|
||||
|
||||
def self.hours_per_day=(value)
|
||||
@@hours_per_day = value
|
||||
end
|
||||
|
||||
def self.days_per_month
|
||||
@@days_per_month
|
||||
end
|
||||
|
||||
def self.days_per_month=(value)
|
||||
@@days_per_month = value
|
||||
end
|
||||
|
||||
# Given a string representation of elapsed time,
|
||||
# return an integer (or float, if fractions of a
|
||||
# second are input)
|
||||
def parse(string, opts = {})
|
||||
result = calculate_from_words(cleanup(string), opts)
|
||||
!opts[:keep_zero] && result == 0 ? nil : result
|
||||
end
|
||||
|
||||
# Given an integer and an optional format,
|
||||
# returns a formatted string representing elapsed time
|
||||
# rubocop:disable Lint/UselessAssignment
|
||||
def output(seconds, opts = {})
|
||||
int = seconds.to_i
|
||||
seconds = int if seconds - int == 0 # if seconds end with .0
|
||||
|
||||
opts[:format] ||= :default
|
||||
opts[:keep_zero] ||= false
|
||||
|
||||
hours_per_day = opts[:hours_per_day] || ChronicDuration.hours_per_day
|
||||
days_per_month = opts[:days_per_month] || ChronicDuration.days_per_month
|
||||
days_per_week = days_per_month / FULL_WEEKS_PER_MONTH
|
||||
|
||||
years = months = weeks = days = hours = minutes = 0
|
||||
|
||||
decimal_places = seconds.to_s.split(".").last.length if seconds.is_a?(Float)
|
||||
|
||||
minute = 60
|
||||
hour = 60 * minute
|
||||
day = hours_per_day * hour
|
||||
month = days_per_month * day
|
||||
year = 31_557_600
|
||||
|
||||
if seconds >= 31_557_600 && seconds % year < seconds % month
|
||||
years = seconds / year
|
||||
months = seconds % year / month
|
||||
days = seconds % year % month / day
|
||||
hours = seconds % year % month % day / hour
|
||||
minutes = seconds % year % month % day % hour / minute
|
||||
seconds = seconds % year % month % day % hour % minute
|
||||
elsif seconds >= 60
|
||||
minutes = (seconds / 60).to_i
|
||||
seconds %= 60
|
||||
if minutes >= 60
|
||||
hours = (minutes / 60).to_i
|
||||
minutes = (minutes % 60).to_i
|
||||
if !opts[:limit_to_hours] && (hours >= hours_per_day)
|
||||
days = (hours / hours_per_day).to_i
|
||||
hours = (hours % hours_per_day).to_i
|
||||
if opts[:weeks]
|
||||
if days >= days_per_week
|
||||
weeks = (days / days_per_week).to_i
|
||||
days = (days % days_per_week).to_i
|
||||
if weeks >= FULL_WEEKS_PER_MONTH
|
||||
months = (weeks / FULL_WEEKS_PER_MONTH).to_i
|
||||
weeks = (weeks % FULL_WEEKS_PER_MONTH).to_i
|
||||
end
|
||||
end
|
||||
elsif days >= days_per_month
|
||||
months = (days / days_per_month).to_i
|
||||
days = (days % days_per_month).to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
joiner = opts.fetch(:joiner) { " " }
|
||||
process = nil
|
||||
|
||||
case opts[:format]
|
||||
when :micro
|
||||
dividers = {
|
||||
years: "y", months: "mo", weeks: "w", days: "d", hours: "h", minutes: "m", seconds: "s"
|
||||
}
|
||||
joiner = ""
|
||||
when :short
|
||||
dividers = {
|
||||
years: "y", months: "mo", weeks: "w", days: "d", hours: "h", minutes: "m", seconds: "s"
|
||||
}
|
||||
when :default
|
||||
dividers = {
|
||||
years: " yr", months: " mo", weeks: " wk", days: " day", hours: " hr", minutes: " min", seconds: " sec",
|
||||
pluralize: true
|
||||
}
|
||||
when :long
|
||||
dividers = {
|
||||
years: " year", months: " month", weeks: " week", days: " day", hours: " hour", minutes: " minute", seconds: " second",
|
||||
pluralize: true
|
||||
}
|
||||
when :chrono
|
||||
dividers = {
|
||||
years: ":", months: ":", weeks: ":", days: ":", hours: ":", minutes: ":", seconds: ":", keep_zero: true
|
||||
}
|
||||
process = lambda do |str|
|
||||
# Pad zeros
|
||||
# Get rid of lead off times if they are zero
|
||||
# Get rid of lead off zero
|
||||
# Get rid of trailing :
|
||||
divider = ":"
|
||||
str.split(divider).map do |n|
|
||||
# add zeros only if n is an integer
|
||||
n.include?(".") ? ("%04.#{decimal_places}f" % n) : ("%02d" % n)
|
||||
end.join(divider).gsub(/^(00:)+/, "").gsub(/^0/, "").gsub(/:$/, "")
|
||||
end
|
||||
joiner = ""
|
||||
end
|
||||
|
||||
result = %i[years months weeks days hours minutes seconds].map do |t|
|
||||
next if t == :weeks && !opts[:weeks]
|
||||
|
||||
num = eval(t.to_s) # rubocop:disable Security/Eval
|
||||
num = ("%.#{decimal_places}f" % num) if num.is_a?(Float) && t == :seconds
|
||||
keep_zero = dividers[:keep_zero]
|
||||
keep_zero ||= opts[:keep_zero] if t == :seconds
|
||||
humanize_time_unit(num, dividers[t], dividers[:pluralize], keep_zero)
|
||||
end.compact!
|
||||
|
||||
result = result[0...opts[:units]] if opts[:units]
|
||||
|
||||
result = result.join(joiner)
|
||||
|
||||
result = process.call(result) if process
|
||||
|
||||
result.empty? ? nil : result
|
||||
end
|
||||
# rubocop:enable Lint/UselessAssignment
|
||||
|
||||
private
|
||||
|
||||
def humanize_time_unit(number, unit, pluralize, keep_zero)
|
||||
return nil if number == 0 && !keep_zero
|
||||
|
||||
res = "#{number}#{unit}"
|
||||
# A poor man's pluralizer
|
||||
res << "s" if (number != 1) && pluralize
|
||||
res
|
||||
end
|
||||
|
||||
def calculate_from_words(string, opts)
|
||||
val = 0
|
||||
words = string.split
|
||||
words.each_with_index do |v, k|
|
||||
next unless v&.match?(float_matcher)
|
||||
|
||||
val += (convert_to_number(v) * duration_units_seconds_multiplier(
|
||||
words[k + 1] || (opts[:default_unit] || "seconds"), opts
|
||||
))
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
def cleanup(string)
|
||||
res = string.downcase
|
||||
res = filter_by_type(res)
|
||||
res = res.gsub(float_matcher) { |n| " #{n} " }.squeeze(" ").strip
|
||||
filter_through_white_list(res)
|
||||
end
|
||||
|
||||
def convert_to_number(string)
|
||||
string.to_f % 1 > 0 ? string.to_f : string.to_i
|
||||
end
|
||||
|
||||
def duration_units_list
|
||||
%w[seconds minutes hours days weeks months years]
|
||||
end
|
||||
|
||||
def duration_units_seconds_multiplier(unit, opts)
|
||||
return 0 unless duration_units_list.include?(unit)
|
||||
|
||||
hours_per_day = opts[:hours_per_day] || ChronicDuration.hours_per_day
|
||||
days_per_month = opts[:days_per_month] || ChronicDuration.days_per_month
|
||||
days_per_week = days_per_month / FULL_WEEKS_PER_MONTH
|
||||
|
||||
case unit
|
||||
when "years" then 31_557_600
|
||||
when "months" then 3600 * hours_per_day * days_per_month
|
||||
when "weeks" then 3600 * hours_per_day * days_per_week
|
||||
when "days" then 3600 * hours_per_day
|
||||
when "hours" then 3600
|
||||
when "minutes" then 60
|
||||
when "seconds" then 1
|
||||
end
|
||||
end
|
||||
|
||||
# Parse 3:41:59 and return 3 hours 41 minutes 59 seconds
|
||||
def filter_by_type(string)
|
||||
chrono_units_list = duration_units_list.reject { |v| v == "weeks" }
|
||||
|
||||
if string.delete(" ")&.match?(time_matcher)
|
||||
res = []
|
||||
string.delete(" ").split(":").reverse.each_with_index do |v, k|
|
||||
return unless chrono_units_list[k] # rubocop:disable Lint/NonLocalExitFromIterator
|
||||
|
||||
res << "#{v} #{chrono_units_list[k]}"
|
||||
end
|
||||
res = res.reverse.join(" ")
|
||||
else
|
||||
res = string
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def time_matcher
|
||||
/^[0-9]+:[0-9]+(:[0-9]+){0,4}(\.[0-9]*)?$/
|
||||
end
|
||||
|
||||
def float_matcher
|
||||
/[0-9]*\.?[0-9]+/
|
||||
end
|
||||
|
||||
# Get rid of unknown words and map found
|
||||
# words to defined time units
|
||||
def filter_through_white_list(string)
|
||||
res = []
|
||||
string.split.each do |word|
|
||||
if word&.match?(float_matcher)
|
||||
res << word.strip
|
||||
next
|
||||
end
|
||||
stripped_word = word.strip.gsub(/^,/, "").gsub(/,$/, "")
|
||||
if mappings.has_key?(stripped_word)
|
||||
res << mappings[stripped_word]
|
||||
elsif !join_words.include?(stripped_word) and ChronicDuration.raise_exceptions # rubocop:disable Rails/NegateInclude
|
||||
raise DurationParseError, "An invalid word #{word.inspect} was used in the string to be parsed."
|
||||
end
|
||||
end
|
||||
# add '1' at front if string starts with something recognizable but not with a number, like 'day' or 'minute 30sec'
|
||||
res.unshift(1) if !res.empty? && mappings[res[0]]
|
||||
res.join(" ")
|
||||
end
|
||||
|
||||
def mappings
|
||||
{
|
||||
"seconds" => "seconds",
|
||||
"second" => "seconds",
|
||||
"secs" => "seconds",
|
||||
"sec" => "seconds",
|
||||
"s" => "seconds",
|
||||
"minutes" => "minutes",
|
||||
"minute" => "minutes",
|
||||
"mins" => "minutes",
|
||||
"min" => "minutes",
|
||||
"m" => "minutes",
|
||||
"hours" => "hours",
|
||||
"hour" => "hours",
|
||||
"hrs" => "hours",
|
||||
"hr" => "hours",
|
||||
"h" => "hours",
|
||||
"days" => "days",
|
||||
"day" => "days",
|
||||
"dy" => "days",
|
||||
"d" => "days",
|
||||
"weeks" => "weeks",
|
||||
"week" => "weeks",
|
||||
"wks" => "weeks",
|
||||
"wk" => "weeks",
|
||||
"w" => "weeks",
|
||||
"months" => "months",
|
||||
"mo" => "months",
|
||||
"mos" => "months",
|
||||
"month" => "months",
|
||||
"years" => "years",
|
||||
"year" => "years",
|
||||
"yrs" => "years",
|
||||
"yr" => "years",
|
||||
"y" => "years"
|
||||
}
|
||||
end
|
||||
|
||||
def join_words
|
||||
%w[and with plus]
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
@@ -0,0 +1,346 @@
|
||||
# Copied from https://gitlab.com/gitlab-org/ruby/gems/gitlab-chronic-duration
|
||||
# version 0.12.0
|
||||
#
|
||||
# Copyright (c) Henry Poydar
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without
|
||||
# restriction, including without limitation the rights to use,
|
||||
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following
|
||||
# conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# NOTE:
|
||||
# Changes to this file should be kept in sync with
|
||||
# frontend/src/app/shared/helpers/chronic_duration.js.
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe ChronicDuration do
|
||||
describe ".parse" do
|
||||
exemplars = {
|
||||
"1:20" => 60 + 20,
|
||||
"1:20.51" => 60 + 20.51,
|
||||
"4:01:01" => (4 * 3600) + 60 + 1,
|
||||
"3 mins 4 sec" => (3 * 60) + 4,
|
||||
"3 Mins 4 Sec" => (3 * 60) + 4,
|
||||
"2 hrs 20 min" => (2 * 3600) + (20 * 60),
|
||||
"2h20min" => (2 * 3600) + (20 * 60),
|
||||
"6 mos 1 day" => (6 * 30 * 24 * 3600) + (24 * 3600),
|
||||
"1 year 6 mos 1 day" => (1 * 31557600) + (6 * 30 * 24 * 3600) + (24 * 3600),
|
||||
"2.5 hrs" => 2.5 * 3600,
|
||||
"47 yrs 6 mos and 4.5d" => (47 * 31557600) + (6 * 30 * 24 * 3600) + (4.5 * 24 * 3600),
|
||||
"3 weeks and, 2 days" => (3600 * 24 * 7 * 3) + (3600 * 24 * 2),
|
||||
"3 weeks, plus 2 days" => (3600 * 24 * 7 * 3) + (3600 * 24 * 2),
|
||||
"3 weeks with 2 days" => (3600 * 24 * 7 * 3) + (3600 * 24 * 2),
|
||||
"1 month" => 3600 * 24 * 30,
|
||||
"2 months" => 3600 * 24 * 30 * 2,
|
||||
"18 months" => 3600 * 24 * 30 * 18,
|
||||
"1 year 6 months" => (3600 * 24 * (365.25 + (6 * 30))).to_i,
|
||||
"day" => 3600 * 24,
|
||||
"minute 30s" => 90
|
||||
}
|
||||
|
||||
context "when string can't be parsed" do
|
||||
it "returns nil" do
|
||||
expect(described_class.parse("gobblygoo")).to be_nil
|
||||
end
|
||||
|
||||
it "cannot parse zero" do
|
||||
expect(described_class.parse("0")).to be_nil
|
||||
end
|
||||
|
||||
context "when @@raise_exceptions set to true" do
|
||||
it "raises with ChronicDuration::DurationParseError" do
|
||||
described_class.raise_exceptions = true
|
||||
expect { described_class.parse("23 gobblygoos") }.to raise_error(ChronicDuration::DurationParseError)
|
||||
described_class.raise_exceptions = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "returns zero if the string parses as zero and the keep_zero option is true" do
|
||||
expect(described_class.parse("0", keep_zero: true)).to eq(0)
|
||||
end
|
||||
|
||||
it "returns a float if seconds are in decimals" do
|
||||
expect(described_class.parse("12 mins 3.141 seconds")).to be_a(Float)
|
||||
end
|
||||
|
||||
it "returns an integer unless the seconds are in decimals" do
|
||||
expect(described_class.parse("12 mins 3 seconds")).to be_a(Integer)
|
||||
end
|
||||
|
||||
it "is able to parse minutes by default" do
|
||||
expect(described_class.parse("5", default_unit: "minutes")).to eq(300)
|
||||
end
|
||||
|
||||
exemplars.each do |k, v|
|
||||
it "parses a duration like #{k}" do
|
||||
expect(described_class.parse(k)).to eq(v)
|
||||
end
|
||||
end
|
||||
|
||||
context "with :hours_per_day and :days_per_month params" do
|
||||
it "uses provided :hours_per_day" do
|
||||
expect(described_class.parse("1d", hours_per_day: 24)).to eq(24 * 60 * 60)
|
||||
expect(described_class.parse("1d", hours_per_day: 8)).to eq(8 * 60 * 60)
|
||||
end
|
||||
|
||||
it "uses provided :days_per_month" do
|
||||
expect(described_class.parse("1mo", days_per_month: 30)).to eq(30 * 24 * 60 * 60)
|
||||
expect(described_class.parse("1mo", days_per_month: 20)).to eq(20 * 24 * 60 * 60)
|
||||
|
||||
expect(described_class.parse("1w", days_per_month: 30)).to eq(7 * 24 * 60 * 60)
|
||||
expect(described_class.parse("1w", days_per_month: 20)).to eq(5 * 24 * 60 * 60)
|
||||
end
|
||||
|
||||
it "uses provided both :hours_per_day and :days_per_month" do
|
||||
expect(described_class.parse("1mo", days_per_month: 30, hours_per_day: 24)).to eq(30 * 24 * 60 * 60)
|
||||
expect(described_class.parse("1mo", days_per_month: 20, hours_per_day: 8)).to eq(20 * 8 * 60 * 60)
|
||||
|
||||
expect(described_class.parse("1w", days_per_month: 30, hours_per_day: 24)).to eq(7 * 24 * 60 * 60)
|
||||
expect(described_class.parse("1w", days_per_month: 20, hours_per_day: 8)).to eq(5 * 8 * 60 * 60)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".output" do
|
||||
exemplars = {
|
||||
(60 + 20) =>
|
||||
{
|
||||
micro: "1m20s",
|
||||
short: "1m 20s",
|
||||
default: "1 min 20 secs",
|
||||
long: "1 minute 20 seconds",
|
||||
chrono: "1:20"
|
||||
},
|
||||
(60 + 20.51) =>
|
||||
{
|
||||
micro: "1m20.51s",
|
||||
short: "1m 20.51s",
|
||||
default: "1 min 20.51 secs",
|
||||
long: "1 minute 20.51 seconds",
|
||||
chrono: "1:20.51"
|
||||
},
|
||||
(60 + 20.51928) =>
|
||||
{
|
||||
micro: "1m20.51928s",
|
||||
short: "1m 20.51928s",
|
||||
default: "1 min 20.51928 secs",
|
||||
long: "1 minute 20.51928 seconds",
|
||||
chrono: "1:20.51928"
|
||||
},
|
||||
((4 * 3600) + 60 + 1) =>
|
||||
{
|
||||
micro: "4h1m1s",
|
||||
short: "4h 1m 1s",
|
||||
default: "4 hrs 1 min 1 sec",
|
||||
long: "4 hours 1 minute 1 second",
|
||||
chrono: "4:01:01"
|
||||
},
|
||||
((2 * 3600) + (20 * 60)) =>
|
||||
{
|
||||
micro: "2h20m",
|
||||
short: "2h 20m",
|
||||
default: "2 hrs 20 mins",
|
||||
long: "2 hours 20 minutes",
|
||||
chrono: "2:20"
|
||||
},
|
||||
((2 * 3600) + (20 * 60)) =>
|
||||
{
|
||||
micro: "2h20m",
|
||||
short: "2h 20m",
|
||||
default: "2 hrs 20 mins",
|
||||
long: "2 hours 20 minutes",
|
||||
chrono: "2:20:00"
|
||||
},
|
||||
((6 * 30 * 24 * 3600) + (24 * 3600)) =>
|
||||
{
|
||||
micro: "6mo1d",
|
||||
short: "6mo 1d",
|
||||
default: "6 mos 1 day",
|
||||
long: "6 months 1 day",
|
||||
chrono: "6:01:00:00:00" # Yuck. FIXME
|
||||
},
|
||||
((365.25 * 24 * 3600) + (24 * 3600)).to_i =>
|
||||
{
|
||||
micro: "1y1d",
|
||||
short: "1y 1d",
|
||||
default: "1 yr 1 day",
|
||||
long: "1 year 1 day",
|
||||
chrono: "1:00:01:00:00:00"
|
||||
},
|
||||
((3 * 365.25 * 24 * 3600) + (24 * 3600)).to_i =>
|
||||
{
|
||||
micro: "3y1d",
|
||||
short: "3y 1d",
|
||||
default: "3 yrs 1 day",
|
||||
long: "3 years 1 day",
|
||||
chrono: "3:00:01:00:00:00"
|
||||
},
|
||||
(3600 * 24 * 30 * 18) =>
|
||||
{
|
||||
micro: "18mo",
|
||||
short: "18mo",
|
||||
default: "18 mos",
|
||||
long: "18 months",
|
||||
chrono: "18:00:00:00:00"
|
||||
}
|
||||
}
|
||||
|
||||
exemplars.each do |k, v|
|
||||
v.each do |key, val|
|
||||
it "properly outputs a duration of #{k} seconds as #{val} using the #{key} format option" do
|
||||
expect(described_class.output(k, format: key)).to eq(val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
keep_zero_exemplars = {
|
||||
true =>
|
||||
{
|
||||
micro: "0s",
|
||||
short: "0s",
|
||||
default: "0 secs",
|
||||
long: "0 seconds",
|
||||
chrono: "0"
|
||||
},
|
||||
false =>
|
||||
{
|
||||
micro: nil,
|
||||
short: nil,
|
||||
default: nil,
|
||||
long: nil,
|
||||
chrono: "0"
|
||||
}
|
||||
}
|
||||
|
||||
keep_zero_exemplars.each do |k, v|
|
||||
v.each do |key, val|
|
||||
it "outputs properly a duration of 0 seconds as #{val.nil? ? 'nil' : val} using the #{key} format option, " \
|
||||
"if the keep_zero option is #{k}" do
|
||||
expect(described_class.output(0, format: key, keep_zero: k)).to eq(val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "returns weeks when needed" do
|
||||
expect(described_class.output(45 * 24 * 60 * 60, weeks: true)).to match(/.*wk.*/)
|
||||
end
|
||||
|
||||
it "returns hours and minutes only when :hours_only option specified" do
|
||||
expect(described_class.output((395 * 24 * 60 * 60) + (15 * 60), limit_to_hours: true)).to eq("9480 hrs 15 mins")
|
||||
end
|
||||
|
||||
context "with :hours_per_day and :days_per_month params" do
|
||||
it "uses provided :hours_per_day" do
|
||||
expect(described_class.output(24 * 60 * 60, hours_per_day: 24)).to eq("1 day")
|
||||
expect(described_class.output(24 * 60 * 60, hours_per_day: 8)).to eq("3 days")
|
||||
end
|
||||
|
||||
it "uses provided :days_per_month" do
|
||||
expect(described_class.output(7 * 24 * 60 * 60, weeks: true, days_per_month: 30)).to eq("1 wk")
|
||||
expect(described_class.output(7 * 24 * 60 * 60, weeks: true, days_per_month: 20)).to eq("1 wk 2 days")
|
||||
end
|
||||
|
||||
it "uses provided both :hours_per_day and :days_per_month" do
|
||||
expect(described_class.output(7 * 24 * 60 * 60, weeks: true, days_per_month: 30, hours_per_day: 24)).to eq("1 wk")
|
||||
expect(described_class.output(5 * 8 * 60 * 60, weeks: true, days_per_month: 20, hours_per_day: 8)).to eq("1 wk")
|
||||
end
|
||||
|
||||
it "uses provided params alongside with :weeks when converting to months" do
|
||||
expect(described_class.output(30 * 24 * 60 * 60, days_per_month: 30, hours_per_day: 24)).to eq("1 mo")
|
||||
expect(described_class.output(30 * 24 * 60 * 60, days_per_month: 30, hours_per_day: 24, weeks: true)).to eq("1 mo 2 days")
|
||||
|
||||
expect(described_class.output(20 * 8 * 60 * 60, days_per_month: 20, hours_per_day: 8)).to eq("1 mo")
|
||||
expect(described_class.output(20 * 8 * 60 * 60, days_per_month: 20, hours_per_day: 8, weeks: true)).to eq("1 mo")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns the specified number of units if provided" do
|
||||
expect(described_class.output((4 * 3600) + 60 + 1, units: 2)).to eq("4 hrs 1 min")
|
||||
expect(described_class.output((6 * 30 * 24 * 3600) + (24 * 3600) + 3600 + 60 + 1,
|
||||
units: 3,
|
||||
format: :long)).to eq("6 months 1 day 1 hour")
|
||||
end
|
||||
|
||||
context "when the format is not specified" do
|
||||
it "uses the default format" do
|
||||
expect(described_class.output((2 * 3600) + (20 * 60))).to eq("2 hrs 20 mins")
|
||||
end
|
||||
end
|
||||
|
||||
exemplars.each do |seconds, format_spec|
|
||||
format_spec.each_key do |format|
|
||||
it "outputs a duration for #{seconds} that parses back to the same thing when using the #{format} format" do
|
||||
expect(described_class.parse(
|
||||
described_class.output(seconds, format:, use_complete_matcher: true)
|
||||
)).to eq(seconds)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "uses user-specified joiner if provided" do
|
||||
expect(described_class.output((2 * 3600) + (20 * 60), joiner: ", ")).to eq("2 hrs, 20 mins")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".filter_by_type" do
|
||||
it "receives a chrono-formatted time like 3:14 and return a human time like 3 minutes 14 seconds" do
|
||||
expect(described_class.instance_eval("filter_by_type('3:14')", __FILE__, __LINE__)).to eq("3 minutes 14 seconds")
|
||||
end
|
||||
|
||||
it "receives chrono-formatted time like 12:10:14 and return a human time like 12 hours 10 minutes 14 seconds" do
|
||||
expect(described_class.instance_eval("filter_by_type('12:10:14')", __FILE__,
|
||||
__LINE__ - 1)).to eq("12 hours 10 minutes 14 seconds")
|
||||
end
|
||||
|
||||
it "returns the input if it's not a chrono-formatted time" do
|
||||
expect(described_class.instance_eval("filter_by_type('4 hours')", __FILE__, __LINE__)).to eq("4 hours")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".cleanup" do
|
||||
it "cleans up extraneous words" do
|
||||
expect(described_class.instance_eval("cleanup('4 days and 11 hours')", __FILE__, __LINE__)).to eq("4 days 11 hours")
|
||||
end
|
||||
|
||||
it "cleans up extraneous spaces" do
|
||||
expect(described_class.instance_eval("cleanup(' 4 days and 11 hours')", __FILE__, __LINE__)).to eq("4 days 11 hours")
|
||||
end
|
||||
|
||||
it "inserts spaces where there aren't any" do
|
||||
expect(described_class.instance_eval("cleanup('4m11.5s')", __FILE__, __LINE__)).to eq("4 minutes 11.5 seconds")
|
||||
end
|
||||
end
|
||||
|
||||
describe "work week" do
|
||||
before do
|
||||
allow(described_class).to receive_messages(
|
||||
hours_per_day: 8,
|
||||
days_per_month: 20
|
||||
)
|
||||
end
|
||||
|
||||
it "parses knowing the work week" do
|
||||
week = described_class.parse("5d")
|
||||
expect(described_class.parse("40h")).to eq(week)
|
||||
expect(described_class.parse("1w")).to eq(week)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user