Merge pull request #18583 from opf/feature/61896-overview-of-time-logged-per-day-per-user-in-pdf-timesheet

[#61896] Overview of time logged per day per user in PDF timesheet
This commit is contained in:
Andrej Sandorf
2025-04-17 10:47:05 +02:00
committed by GitHub
8 changed files with 347 additions and 65 deletions
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -85,6 +87,10 @@ module WorkPackage::PDFExport::Common::Common
"<link anchor=\"#{anchor}\">#{caption}</link>"
end
def make_link_href(href, caption)
"<link href=\"#{href}\">#{caption}</link>"
end
def link_target_at_current_y(id)
pdf_dest = pdf.dest_xyz(0, pdf.y)
pdf.add_dest(id.to_s, pdf_dest)
@@ -295,7 +301,7 @@ module WorkPackage::PDFExport::Common::Common
end
def make_link_href_cell(href, caption)
"<color rgb='#{styles.link_color}'><link href='#{href}'>#{caption}</link></color>"
"<color rgb='#{styles.link_color}'>#{make_link_href(href, caption)}</color>"
end
def get_id_column_cell(work_package, value)
@@ -310,4 +316,17 @@ module WorkPackage::PDFExport::Common::Common
def wp_status_prawn_color(work_package)
work_package.status.color&.hexcode&.sub("#", "") || "F0F0F0"
end
def add_pdf_table_anchors(prawn_table)
# prawn table does not support anchors, so we have to add them manually,
# @see `lib/open_project/patches/prawn_table_cell.rb` for cell_id attribute
prawn_table.before_rendering_page do |cells|
cells.each do |cell|
if cell.respond_to?(:cell_id) && cell.cell_id.present?
pdf_dest = @pdf.dest_xyz(@pdf.bounds.absolute_left, @pdf.y + cell.y)
@pdf.add_dest(cell.cell_id, pdf_dest)
end
end
end
end
end
+18
View File
@@ -103,6 +103,11 @@ module ChronicDuration
if %i[hours_only hours_and_minutes].include?(opts[:format])
hours = seconds / 3600.0
seconds = 0
elsif opts[:format] == :hours_colon_minutes
hours = (seconds / hour).to_i
minutes = (seconds % hour / minute).to_i
minutes += 1 if (seconds % hour % minute) > 0
seconds = 0
elsif seconds >= SECONDS_PER_YEAR && seconds % year < seconds % month
years = seconds / year
months = seconds % year / month
@@ -187,6 +192,19 @@ module ChronicDuration
minutes = ((hours % 1) * 60).round
hours = hours.floor
when :hours_colon_minutes
dividers = {
hours: ":", minutes: "", keep_zero: true
}
process = lambda do |str|
# Pad zeros on minutes
divider = ":"
result = str.split(divider).each_with_index.map do |n, index|
n.rjust(index > 0 ? 2 : 1, "0")
end.join(divider)
"#{result} h"
end
joiner = ""
when :chrono
dividers = {
years: ":", months: ":", weeks: ":", days: ":", hours: ":", minutes: ":", seconds: ":", keep_zero: true
@@ -0,0 +1,35 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject::Patches::PrawnTableCellPatch
attr_accessor :cell_id
end
Prawn::Table::Cell.prepend(OpenProject::Patches::PrawnTableCellPatch)
@@ -1,3 +1,33 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "active_storage/filename"
class CostQuery::PDF::ExportTimesheetJob < Exports::ExportJob
@@ -1,3 +1,33 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CostQuery::PDF::TimesheetGenerator
include WorkPackage::PDFExport::Common::Common
include WorkPackage::PDFExport::Common::Attachments
@@ -10,18 +40,22 @@ class CostQuery::PDF::TimesheetGenerator
H1_FONT_SIZE = 26
H1_MARGIN_BOTTOM = 2
HR_MARGIN_BOTTOM = 16
PARAGRAPH_MARGIN_BOTTOM = 16
TABLE_MARGIN_BOTTOM = 28
TABLE_CELL_FONT_SIZE = 10
TABLE_CELL_BORDER_COLOR = "BBBBBB".freeze
TABLE_CELL_BORDER_COLOR = "BBBBBB"
TABLE_CELL_PADDING = 4
COMMENT_FONT_COLOR = "636C76".freeze
COMMENT_FONT_COLOR = "636C76"
H2_FONT_SIZE = 20
H2_MARGIN_BOTTOM = 10
SUM_TABLE_FIRST_COLUMN_WIDTH = 90
SUM_TABLE_MAX_USER_COLUMNS = 6
COLUMN_DATE_WIDTH = 66
COLUMN_DATE_WIDTH = 90
COLUMN_ACTIVITY_WIDTH = 100
COLUMN_HOURS_WIDTH = 60
COLUMN_TIME_WIDTH = 110
COLUMN_WP_WIDTH = 190
COLUMN_WP_WIDTH = 164
attr_accessor :pdf
@@ -113,6 +147,13 @@ class CostQuery::PDF::TimesheetGenerator
.group_by(&:user)
end
def all_users
all_entries
.uniq { |entry| entry.user.id }
.map(&:user)
.sort_by(&:name)
end
def all_entries
@all_entries ||= begin
ids = query
@@ -124,7 +165,7 @@ class CostQuery::PDF::TimesheetGenerator
end
end
def build_table_rows(entries)
def build_table_rows(entries, wants_sum_row)
rows = [table_header_columns]
entries
.group_by(&:spent_on)
@@ -132,7 +173,7 @@ class CostQuery::PDF::TimesheetGenerator
.each do |spent_on, lines|
rows.concat(build_table_day_rows(spent_on, lines))
end
rows.push(build_table_row_sum(entries))
rows.push(build_table_row_sum(entries)) if wants_sum_row
rows
end
@@ -140,29 +181,56 @@ class CostQuery::PDF::TimesheetGenerator
day_rows = []
entries.each do |entry|
day_rows.push(build_table_row(spent_on, entry))
if entry.comments.present?
day_rows.push(build_table_row_comment(entry))
end
day_rows.push(build_table_row_comment(entry)) if entry.comments.present?
end
day_rows.push(build_table_row_day_sum(spent_on, entries)) if entries.length > 1
day_rows
end
def build_table_row(spent_on, entry)
[
{ content: format_date(spent_on), rowspan: entry.comments.present? ? 2 : 1 },
entry.work_package&.subject || "",
{
content: format_date_with_weekday(spent_on),
rowspan: (entry.comments.present? ? 2 : 1),
cell_id: spent_on_day_id(entry.user, spent_on)
},
build_table_subject_cell(entry),
with_times_column? ? format_spent_on_time(entry) : nil,
format_hours(entry.hours || 0),
{ content: format_hours(entry.hours || 0), align: :right },
entry.activity&.name || ""
].compact
end
def build_table_row_day_sum(spent_on, entries)
[
{ content: format_date_with_weekday(spent_on), rowspan: 1 },
"",
with_times_column? ? "" : nil,
{ content: format_sum_time_entries(entries), font_style: :bold, align: :right },
""
].compact
end
def spent_on_day_id(user, spent_on)
"entry_#{user.id}_#{spent_on}"
end
def build_table_subject_cell(entry)
return "" if entry.work_package.nil?
href = url_helpers.work_package_url(entry.work_package)
{
content: "#{make_link_href(href, "##{entry.work_package.id}")} #{entry.work_package.subject || ''}",
inline_format: true
}
end
def build_table_row_sum(entries)
[
{ content: "", rowspan: 1 },
"",
with_times_column? ? "" : nil,
{ content: format_sum_time_entries(entries), font_style: :bold },
{ content: format_sum_time_entries(entries), font_style: :bold, align: :right },
""
].compact
end
@@ -189,7 +257,7 @@ class CostQuery::PDF::TimesheetGenerator
{ content: TimeEntry.human_attribute_name(:spent_on), rowspan: 1 },
I18n.t(:"activerecord.models.work_package"),
with_times_column? ? I18n.t(:"export.timesheet.time") : nil,
TimeEntry.human_attribute_name(:hours),
{ content: TimeEntry.human_attribute_name(:hours), align: :right },
TimeEntry.human_attribute_name(:activity)
].compact
end
@@ -204,7 +272,7 @@ class CostQuery::PDF::TimesheetGenerator
end
end
def build_table(rows, has_sum_row)
def build_table(rows, has_sum_row, with_anchors)
pdf.make_table(
rows,
header: true,
@@ -223,6 +291,7 @@ class CostQuery::PDF::TimesheetGenerator
adjust_borders_spanned_column(table)
adjust_border_header_row(table)
adjust_border_sum_row(table) if has_sum_row
add_pdf_table_anchors(table) if with_anchors
end
end
@@ -263,8 +332,8 @@ class CostQuery::PDF::TimesheetGenerator
end
end
def split_group_rows(table_rows)
measure_table = build_table(table_rows, true)
def split_group_rows(table_rows, has_sum_row)
measure_table = build_table(table_rows, has_sum_row, false)
groups = []
index = 0
while index < table_rows.length
@@ -283,14 +352,23 @@ class CostQuery::PDF::TimesheetGenerator
end
def write_table(user, entries)
rows = build_table_rows(entries)
wants_sum_row = more_than_one_day?(entries)
rows = build_table_rows(entries, wants_sum_row)
# prawn-table does not support splitting a rowspan cell on page break, so we have to merge the first column manually
# for easier handling existing rowspan cells are grouped as one row
grouped_rows = split_group_rows(rows)
grouped_rows = split_group_rows(rows, wants_sum_row)
# start a new page if the username would be printed alone at the end of the page
pdf.start_new_page if available_space_from_bottom < grouped_rows[0][:height] + grouped_rows[1][:height] + username_height
pdf.start_new_page if available_space_from_bottom < grouped_table_height(grouped_rows)
write_username(user)
write_grouped_tables(grouped_rows)
write_grouped_tables(grouped_rows, wants_sum_row)
end
def grouped_table_height(grouped_rows)
grouped_rows[0][:height] + grouped_rows[1][:height] + username_height
end
def more_than_one_day?(entries)
entries.map(&:spent_on).uniq.length > 1
end
def available_space_from_bottom
@@ -298,7 +376,7 @@ class CostQuery::PDF::TimesheetGenerator
pdf.y - margin_bottom
end
def write_grouped_tables(grouped_rows)
def write_grouped_tables(grouped_rows, has_sum_row)
header_row = grouped_rows[0]
current_table = []
current_table_height = 0
@@ -313,15 +391,15 @@ class CostQuery::PDF::TimesheetGenerator
current_table.push(grouped_row)
current_table_height += grouped_row_height
end
write_grouped_row_table(current_table, true)
pdf.move_down(28)
write_grouped_row_table(current_table, has_sum_row)
pdf.move_down(TABLE_MARGIN_BOTTOM)
end
def write_grouped_row_table(grouped_rows, has_sum_row)
current_table = []
merge_first_columns(grouped_rows)
grouped_rows.map! { |row| current_table.concat(row[:rows]) }
build_table(current_table, has_sum_row).draw
build_table(current_table, has_sum_row, true).draw
end
def merge_first_columns(grouped_rows)
@@ -357,21 +435,102 @@ class CostQuery::PDF::TimesheetGenerator
end
def write_overview!
groups = grouped_by_user_entries
return if groups.size <= 1
users = all_users
return if users.size <= 1
write_heading!
write_hr!
write_overview_table!(overview_table_rows(groups))
write_total_sum!
write_sums_tables!(users)
start_new_page_if_needed
end
def write_overview_table!(rows)
pdf.make_table(
def write_sums_tables!(users)
start_date, end_date = all_entries.map(&:spent_on).minmax
num_groups = (users.length / SUM_TABLE_MAX_USER_COLUMNS.to_f).ceil
users
.in_groups(num_groups, false) do |users_chunk|
write_sum_table!(users_chunk.compact, start_date, end_date)
pdf.move_down(TABLE_MARGIN_BOTTOM)
end
end
def write_total_sum!
pdf.formatted_text(
[
{ text: "#{I18n.t('export.timesheet.total_sum')}: ", size: TABLE_CELL_FONT_SIZE },
{ text: format_sum_time_entries(all_entries), size: TABLE_CELL_FONT_SIZE, styles: [:bold] }
]
)
pdf.move_down(PARAGRAPH_MARGIN_BOTTOM)
end
def build_sum_table_footer_rows(users, users_sums)
[
[{ content: I18n.t("export.timesheet.sums_hours"), font_style: :bold }] + users.map do |user|
{ content: format_hours(users_sums[user.id]), align: :right, font_style: :bold }
end
]
end
def build_sum_table_row(date, users, users_sums)
row = []
users.each do |user|
sum = calc_sum_for_user_on_day(user, date)
users_sums[user.id] = (users_sums[user.id] || 0) + sum
row.push(sum > 0 ? build_sum_table_sum_cell(sum, user, date) : "")
end
return nil unless row.any? { |column| !column.empty? }
[format_date_with_weekday(date)] + row
end
def build_sum_table_sum_cell(sum, user, date)
{
content: make_link_anchor(spent_on_day_id(user, date), format_hours(sum)),
align: :right, inline_format: true
}
end
def format_date_with_weekday(date)
"#{format_date(date)}, #{I18n.l(date.to_date, format: '%a')}"
end
def calc_sum_for_user_on_day(user, date)
all_entries.select { |entry| entry.user == user && entry.spent_on == date }.sum(&:hours)
end
def build_sum_table_rows(users, start_date, end_date)
rows = []
users_sums = {}
(start_date..end_date).each do |date|
row = build_sum_table_row(date, users, users_sums)
rows.push(row) unless row.nil?
end
rows = rows + build_sum_table_footer_rows(users, users_sums) unless rows.empty?
rows
end
def build_sum_table_header_row(users)
[{ content: TimeEntry.human_attribute_name(:spent_on), font_style: :bold }] +
users.map do |user|
{
content: make_link_anchor("user_#{user.id}", user.name),
inline_format: true, font_style: :bold
}
end
end
def write_sum_table!(users, start_date, end_date)
rows = build_sum_table_rows(users, start_date, end_date)
return if rows.empty?
rows.unshift(build_sum_table_header_row(users))
pdf.table(
rows,
header: true,
width: pdf.bounds.width,
column_widths: sum_table_column_widths(users),
cell_style: {
size: TABLE_CELL_FONT_SIZE,
border_color: TABLE_CELL_BORDER_COLOR,
@@ -379,9 +538,13 @@ class CostQuery::PDF::TimesheetGenerator
borders: %i[top bottom left right],
padding: [TABLE_CELL_PADDING, TABLE_CELL_PADDING, TABLE_CELL_PADDING + 2, TABLE_CELL_PADDING]
}
) do |table|
adjust_overview_border_sum_row(table)
end.draw
)
end
def sum_table_column_widths(users)
[SUM_TABLE_FIRST_COLUMN_WIDTH].concat(
[(pdf.bounds.width - SUM_TABLE_FIRST_COLUMN_WIDTH) / users.length] * users.length
)
end
def adjust_overview_border_sum_row(table)
@@ -390,22 +553,6 @@ class CostQuery::PDF::TimesheetGenerator
row.columns(-1).style { |c| c.borders = c.borders - [:left] }
end
def overview_table_rows(groups)
rows = [
[
{ content: TimeEntry.human_attribute_name(:user), font_style: :bold },
{ content: I18n.t("export.timesheet.sum_hours"), font_style: :bold }
]
]
groups.each do |user, entries|
rows.push([user.name, format_sum_time_entries(entries)])
end
total = groups.sum { |_user, entries| entries.sum(&:hours) }
rows.push(["", { content: format_hours(total), font_style: :bold }])
rows
end
def write_heading!
pdf.formatted_text([{ text: heading, size: H1_FONT_SIZE, style: :bold }])
pdf.move_down(H1_MARGIN_BOTTOM)
@@ -416,6 +563,7 @@ class CostQuery::PDF::TimesheetGenerator
end
def write_username(user)
link_target_at_current_y("user_#{user.id}")
pdf.formatted_text([{ text: user.name, size: H2_FONT_SIZE }])
pdf.move_down(H2_MARGIN_BOTTOM)
end
@@ -431,7 +579,7 @@ class CostQuery::PDF::TimesheetGenerator
def format_hours(hours)
return "" if hours.nil? || hours < 0
DurationConverter.output(hours)
DurationConverter.output(hours, format: :hours_colon_minutes)
end
def format_spent_on_time(entry)
+2 -1
View File
@@ -112,7 +112,8 @@ en:
button: "Export PDF timesheet"
timesheet: "Timesheet"
time: "Time"
sum_hours: Sum
sums_hours: Sums
total_sum: Total sum
cost_reports:
title: "Your Cost Reports XLS export"
start_time: "Start time"
@@ -47,6 +47,7 @@ RSpec.describe CostQuery::PDF::TimesheetGenerator do
user: user,
spent_on: Date.new(2024, 12, 0o1),
start_time: 8 * 60,
hours: 28,
time_zone: "UTC")
end
let(:time_entry) do
@@ -124,12 +125,14 @@ RSpec.describe CostQuery::PDF::TimesheetGenerator do
end
def expected_entry_row(t_entry, with_times_column)
[format_date(t_entry.spent_on)].concat(expected_entry_columns(t_entry, with_times_column))
result = [generator.format_date_with_weekday(t_entry.spent_on)]
result.concat(expected_entry_columns(t_entry, with_times_column))
end
def expected_entry_columns(t_entry, with_times_column)
time_column = generator.format_spent_on_time(t_entry)
[
"##{t_entry.work_package.id} ",
t_entry.work_package&.subject || "",
with_times_column && time_column.present? ? time_column : nil,
generator.format_hours(t_entry.hours),
@@ -139,21 +142,35 @@ RSpec.describe CostQuery::PDF::TimesheetGenerator do
end
def expected_overview_page_content
result = [query.name,
TimeEntry.human_attribute_name(:user), I18n.t("export.timesheet.sum_hours")]
time_entries.group_by(&:user).each do |user, entries|
result << user.name
result << generator.format_hours(entries.sum(&:hours))
end
result << generator.format_hours(time_entries.sum(&:hours))
[
query.name,
"#{I18n.t('export.timesheet.total_sum')}: ",
generator.format_hours(time_entries.sum(&:hours)),
TimeEntry.human_attribute_name(:spent_on),
user.name,
time_entry_user.name,
generator.format_date_with_weekday(user_time_entry.spent_on),
generator.format_hours(user_time_entry.hours), generator.format_hours(time_entry.hours + other_time_entry.hours),
generator.format_date_with_weekday(time_entry_with_comment.spent_on),
generator.format_hours(time_entry_with_comment.hours),
generator.format_date_with_weekday(time_entry_without_time.spent_on),
generator.format_hours(time_entry_without_time.hours),
I18n.t("export.timesheet.sums_hours"),
generator.format_hours(time_entries.select { |entry| entry.user == user }.sum(&:hours)),
generator.format_hours(time_entries.select { |entry| entry.user == time_entry_user }.sum(&:hours))
]
end
def expected_first_user_table(with_times_column)
[
user.name,
*expected_table_header(with_times_column),
*expected_entry_row(user_time_entry, with_times_column),
*expected_sum_row(user, with_times_column)
*expected_entry_row(user_time_entry, with_times_column)
]
end
@@ -161,9 +178,10 @@ RSpec.describe CostQuery::PDF::TimesheetGenerator do
[
time_entry.user.name,
*expected_table_header(with_times_column),
format_date(time_entry.spent_on), # merged date rows
generator.format_date_with_weekday(time_entry.spent_on), # merged date rows
*expected_entry_columns(time_entry, with_times_column),
*expected_entry_columns(other_time_entry, with_times_column),
generator.format_hours(time_entry.hours + other_time_entry.hours),
*expected_entry_row(time_entry_with_comment, with_times_column),
*expected_entry_row(time_entry_without_time, with_times_column),
*expected_sum_row(time_entry.user, with_times_column)
+14 -1
View File
@@ -31,7 +31,7 @@
require "rspec_helper"
require "chronic_duration"
INACCURATE_FORMATS = %i[days_and_hours hours_only hours_and_minutes].freeze
INACCURATE_FORMATS = %i[days_and_hours hours_only hours_and_minutes hours_colon_minutes].freeze
RSpec.describe ChronicDuration do
describe ".parse" do
@@ -152,6 +152,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "0.02h",
hours_only: "0.02h",
hours_and_minutes: "1m",
hours_colon_minutes: "0:02 h",
chrono: "1:20"
},
(60 + 20.51) =>
@@ -163,6 +164,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "0.02h",
hours_only: "0.02h",
hours_and_minutes: "1m",
hours_colon_minutes: "0:02 h",
chrono: "1:20.51"
},
(60 + 20.51928) =>
@@ -174,6 +176,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "0.02h",
hours_only: "0.02h",
hours_and_minutes: "1m",
hours_colon_minutes: "0:02 h",
chrono: "1:20.51928"
},
((4 * 3600) + 60 + 1) =>
@@ -185,6 +188,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "4.02h",
hours_only: "4.02h",
hours_and_minutes: "4h 1m",
hours_colon_minutes: "4:02 h",
chrono: "4:01:01"
},
((2 * 3600) + (20 * 60)) =>
@@ -196,6 +200,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "2.33h",
hours_only: "2.33h",
hours_and_minutes: "2h 20m",
hours_colon_minutes: "2:20 h",
chrono: "2:20:00"
},
((8 * 24 * 3600) + (3 * 3600) + (30 * 60)) =>
@@ -207,6 +212,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "8d 3.5h",
hours_only: "195.5h",
hours_and_minutes: "195h 30m",
hours_colon_minutes: "195:30 h",
chrono: "8:03:30:00"
},
((6 * 30 * 24 * 3600) + (24 * 3600)) =>
@@ -218,6 +224,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "181d 0h",
hours_only: "4344h",
hours_and_minutes: "4344h",
hours_colon_minutes: "4344:00 h",
chrono: "6:01:00:00:00" # Yuck. FIXME
},
((365.25 * 24 * 3600) + (24 * 3600)).to_i =>
@@ -229,6 +236,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "366d 0h",
hours_only: "8790h",
hours_and_minutes: "8790h",
hours_colon_minutes: "8790:00 h",
chrono: "1:00:01:00:00:00"
},
((3 * 365.25 * 24 * 3600) + (24 * 3600)).to_i =>
@@ -240,6 +248,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "1096d 0h",
hours_only: "26322h",
hours_and_minutes: "26322h",
hours_colon_minutes: "26322:00 h",
chrono: "3:00:01:00:00:00"
},
((6 * 365.25 * 24 * 3600) + (3 * 3600)).to_i =>
@@ -251,6 +260,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "2191d 3h",
hours_only: "52599h",
hours_and_minutes: "52599h",
hours_colon_minutes: "52599:00 h",
chrono: "6:00:00:03:00:00"
},
(3600 * 24 * 30 * 18) =>
@@ -262,6 +272,7 @@ RSpec.describe ChronicDuration do
days_and_hours: "540d 0h",
hours_only: "12960h",
hours_and_minutes: "12960h",
hours_colon_minutes: "12960:00 h",
chrono: "18:00:00:00:00"
}
}
@@ -282,6 +293,7 @@ RSpec.describe ChronicDuration do
default: "0 secs",
long: "0 seconds",
days_and_hours: "0h",
hours_colon_minutes: "0:00 h",
chrono: "0"
},
false =>
@@ -291,6 +303,7 @@ RSpec.describe ChronicDuration do
default: nil,
long: nil,
days_and_hours: "0h",
hours_colon_minutes: "0:00 h",
chrono: "0"
}
}