diff --git a/app/models/work_package/pdf_export/common/common.rb b/app/models/work_package/pdf_export/common/common.rb index 4fe9273eed3..0afd576868c 100644 --- a/app/models/work_package/pdf_export/common/common.rb +++ b/app/models/work_package/pdf_export/common/common.rb @@ -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 "#{caption}" end + def make_link_href(href, caption) + "#{caption}" + 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) - "#{caption}" + "#{make_link_href(href, caption)}" 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 diff --git a/lib/chronic_duration.rb b/lib/chronic_duration.rb index f3a03d6ef8a..de6db62414d 100644 --- a/lib/chronic_duration.rb +++ b/lib/chronic_duration.rb @@ -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 diff --git a/lib/open_project/patches/prawn_table_cell.rb b/lib/open_project/patches/prawn_table_cell.rb new file mode 100644 index 00000000000..b33a039f8b4 --- /dev/null +++ b/lib/open_project/patches/prawn_table_cell.rb @@ -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) diff --git a/modules/reporting/app/workers/cost_query/pdf/export_timesheet_job.rb b/modules/reporting/app/workers/cost_query/pdf/export_timesheet_job.rb index 058cb206136..f92c1677680 100644 --- a/modules/reporting/app/workers/cost_query/pdf/export_timesheet_job.rb +++ b/modules/reporting/app/workers/cost_query/pdf/export_timesheet_job.rb @@ -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 diff --git a/modules/reporting/app/workers/cost_query/pdf/timesheet_generator.rb b/modules/reporting/app/workers/cost_query/pdf/timesheet_generator.rb index 57a603151a7..a2cfd68b98d 100644 --- a/modules/reporting/app/workers/cost_query/pdf/timesheet_generator.rb +++ b/modules/reporting/app/workers/cost_query/pdf/timesheet_generator.rb @@ -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) diff --git a/modules/reporting/config/locales/en.yml b/modules/reporting/config/locales/en.yml index 27a86d35c3c..9646f9414eb 100644 --- a/modules/reporting/config/locales/en.yml +++ b/modules/reporting/config/locales/en.yml @@ -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" diff --git a/modules/reporting/spec/workers/cost_query/pdf/timesheet_generator_spec.rb b/modules/reporting/spec/workers/cost_query/pdf/timesheet_generator_spec.rb index 3b0a6ed3e7a..9dd89aa1f5e 100644 --- a/modules/reporting/spec/workers/cost_query/pdf/timesheet_generator_spec.rb +++ b/modules/reporting/spec/workers/cost_query/pdf/timesheet_generator_spec.rb @@ -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) diff --git a/spec/lib/chronic_duration_spec.rb b/spec/lib/chronic_duration_spec.rb index a1f5f9d75ce..b577af35550 100644 --- a/spec/lib/chronic_duration_spec.rb +++ b/spec/lib/chronic_duration_spec.rb @@ -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" } }