Как преобразовать дату во время разбора документа .xls с помощью Rails?

Я использую Rails 5. Я хочу разобрать .xls(не путать с .xlsx doc), используя код ниже

  book = Roo::Spreadsheet.open(file_location)
  sheet = book.sheet(0)
  text = sheet.to_csv
  csv = CSV.parse(text)

  arr_of_arrs = csv
  text_content = ""
  arr_of_arrs.each do |arr|
    arr.map!{|v| v && v.to_f < 1 && v.to_f > 0 ? TimeFormattingHelper.time_as_str(v.to_f * 24 * 3600 * 1000) : v}
    text_content = "#{text_content}\n#{arr.join("\t")}"
  end

Вот метод, который я упоминал выше

  def time_as_str(time_in_ms)
    regex = /^(0*:?)*0*/
    Time.at(time_in_ms.to_f/1000).utc.strftime("%H:%M:%S.%1N").sub!(regex, '')
  end

В одной области, с которой я столкнулся, есть ячейка, которая появляется в моем .xls doc как

24:08:00

обрабатывается как

1904-01-02T00:08:00+00:00

с кодом выше. Как разобрать значение, которое я вижу на экране? То есть, как преобразовать значение даты в значение времени?

В качестве примера из другого документа Excel, ячейки, которая отображается как

24:02:00

анализируется моим кодом выше как

1899-12-31T00:02:00+00:00

Ответ 1

Кажется, ваш .xls находится в 1904 date system, и Roo не может различать, что такое Duration и что такое DateTime, поэтому вам нужно будет вычесть базовую дату 1904-01-01 на значение ячейки. Как ни странно, в случае системы дат 1900 вы должны вычесть базовую дату 1899-12-30 из-за ошибки в Lotus 1-2-3, которую Microsoft реплицировала в Excel для обеспечения совместимости.

Вот метод, который преобразует DateTime, считанный из электронной таблицы, в продолжительность в соответствии с базовой датой:

def duration_as_str(datetime, base_date)
  total_seconds = DateTime.parse(datetime).to_i - base_date.to_i
  hours = total_seconds / (60 * 60)
  minutes = (total_seconds / 60) % 60
  seconds = total_seconds % 60
  "%d:%02d:%02d" % [hours, minutes, seconds]
end

Протестируйте его:

irb(main):019:0> duration_as_str("1904-01-02T00:08:00+00:00", DateTime.new(1904, 1, 1))
=> "24:08:00"
irb(main):020:0> duration_as_str("1899-12-31T00:02:00+00:00", DateTime.new(1899, 12, 30))
=> "24:02:00"

Вы можете использовать book.workbook.date_base.year для определения системы данных электронной таблицы, а затем просто добавить еще один map внутри цикла each:

book = Roo::Spreadsheet.open(file_location)
sheet = book.sheet(0)
text = sheet.to_csv
csv = CSV.parse(text)

base_date = book.workbook.date_base.year == 1904 ? DateTime.new(1904, 1, 1) : DateTime.new(1899, 12, 30)
arr_of_arrs = csv
text_content = ""
arr_of_arrs.each do |arr|
  arr.map!{|v| v && v.to_f < 1 && v.to_f > 0 ? TimeFormattingHelper.time_as_str(v.to_f * 24 * 3600 * 1000) : v}
  arr.map!{|v| v =~ /^(1904|1899)-/ ? duration_as_str(v, base_date) : v}
  text_content = "#{text_content}\n#{arr.join("\t")}"
end

Ответ 2

Вы можете использовать что-то вроде ниже и написать собственный синтаксический анализатор для этой строки.

duration = 0

"24:08:01".split(":").each_with_index do |value, i|
  if i == 0
    duration += value.to_i.hours
  elsif i == 1
    duration += value.to_i.minutes
  else
    duration += value.to_i.seconds
  end
end

duration.value => 86881 (duration in seconds)

Этот анализатор примет формат hours:minutes:seconds и вернет экземпляр ActiveSupport::Duration. Затем duration.value даст вам количество секунд.

Ответ 3

Вам нужно прочитать внутреннее значение ячейки вместо форматированного значения. Отформатированное значение записывается в csv, когда вы используете to_csv

Чтобы прочитать внутреннее значение, вам нужно использовать либо метод sheet objects excelx_value, либо метод row object cell_value.

Эти методы возвращают значение в float (days). Вот пример с использованием cell_value путем итерации по строкам, если не считать заголовка и первого столбца со значением, которое нужно преобразовать.

Использование Roo 2.7.1 ( аналогичные методы существуют в старой версии)

book = Roo::Spreadsheet.open(file_location)
sheet = book.sheet(0)

formatted_times = []
time_column_index = 0

sheet.each_row_streaming do |row|
  time_in_days = row[time_column_index].cell_value  
  formatted_times << time_as_str(time_in_days.to_f * 24 * 3600) 
end

def time_as_str(t)
  minutes, seconds = t.divmod(60)
  hours, minutes = minutes.divmod(60)
  "%02d:%02d:%02d" % [hours, minutes, seconds]
end

# eg: time_in_days = 1.0169444444444444
# formatted_time = "24:24:24"

Ответ 4

Сначала я попробую перефразировать то, что вы хотите выполнить.

  • Вы хотите "проанализировать значение, которое вы видите на экране", но я не уверен, что это 24:08:00 или 1904-01-02T00:08:00+00:00. Я предполагаю, что это первый.

  • Вы хотите преобразовать значение даты в значение времени. Я не уверен, что вы действительно хотите, чтобы output var был Time, Date, DateTime или просто String. Я предполагаю, что для вас это нормально, как String, но это небольшая проблема.

С этим я предполагаю, что то, что вы обычно видите как HH:MM:SS в Excel, вы хотите получить как "HH:MM:SS" в Rails, независимо от HH, являющегося > 23. Например, 24:08:00 в Excel превратится в "24:08:00" в Rails.

Два, казалось бы, несогласных случая, о которых вы сообщаете, скорее всего, связаны с двумя файлами .xls, имеющими разные системы дат .

Чтобы получить желаемый результат, у вас есть два варианта:

  • Используйте to_csv, на результат которого влияет система дат файла Excel. В этом случае вы должны вычесть base_date, как это сделал Хелдер Перейра.

  • Непосредственно получить числовое значение из Excel, на которое не влияет система дат. В этом случае код проще, так как вам нужно только одно преобразование (функция days2str ниже).

Код (по модулю незначительных настроек)

def days2str(days)
    days_int = int(days)
    hours = ( days - days_int ) * 24
    hours_int = int(hours)
    seconds = ( hours - hours_int ) * 3600
    seconds_int = int(seconds)
    hours_int = hours_int + 24 * days_int
    format("%d:%02d:%02d", hours_int, minutes_int, seconds_int)
end

def is_date(v)
    # Define the checking function
end

require 'spreadsheet'    
Spreadsheet.open('MyTestSheet.xls') do |book|
    book.worksheet('Sheet1').each do |row|
        break if row[0].nil?
        puts row.join(',')
        row.map!{|v| is_date(v) ? days2str(v) : v }
        text_content = "#{text_content}\n#{arr.join("\t")}"
    end
end