web-dev-qa-db-fra.com

Comment générer une plage de temps lisible par l'homme à l'aide de Ruby on rails

J'essaie de trouver le meilleur moyen de générer la sortie suivante

<name> job took 30 seconds
<name> job took 1 minute and 20 seconds
<name> job took 30 minutes and 1 second
<name> job took 3 hours and 2 minutes

J'ai commencé ce code

def time_range_details
  time = (self.created_at..self.updated_at).count
  sync_time = case time 
    when 0..60 then "#{time} secs"       
    else "#{time/60} minunte(s) and #{time-min*60} seconds"
  end
end

Y at-il un moyen plus efficace de le faire? Il semble que beaucoup de code redondant pour quelque chose de super simple. 

Une autre utilisation pour ceci est:

<title> was posted 20 seconds ago
<title> was posted 2 hours ago

Le code utilisé est similaire, mais j'utilise plutôt Time.now:

def time_since_posted
  time = (self.created_at..Time.now).count
  ...
  ...
end
26
csanz

Si vous avez besoin de quelque chose de plus "précis" que distance_of_time_in_words , vous pouvez écrire quelque chose dans le sens suivant:

def humanize secs
  [[60, :seconds], [60, :minutes], [24, :hours], [Float::INFINITY, :days]].map{ |count, name|
    if secs > 0
      secs, n = secs.divmod(count)

      "#{n.to_i} #{name}" unless n.to_i==0
    end
  }.compact.reverse.join(' ')
end

p humanize 1234
#=>"20 minutes 34 seconds"
p humanize 12345
#=>"3 hours 25 minutes 45 seconds"
p humanize 123456
#=>"1 days 10 hours 17 minutes 36 seconds"
p humanize(Time.now - Time.local(2010,11,5))
#=>"4 days 18 hours 24 minutes 7 seconds"

Oh, une remarque sur votre code:

(self.created_at..self.updated_at).count

est vraiment mauvaise façon de faire la différence. Utilisez simplement:

self.updated_at - self.created_at
58

Il existe deux méthodes dans DateHelper qui peuvent vous donner ce que vous voulez:

  1. time_ago_in_words

    time_ago_in_words( 1234.seconds.from_now ) #=> "21 minutes"
    
    time_ago_in_words( 12345.seconds.ago )     #=> "about 3 hours"
    
  2. distance_du_temps_en_words

    distance_of_time_in_words( Time.now, 1234.seconds.from_now ) #=> "21 minutes"
    
    distance_of_time_in_words( Time.now, 12345.seconds.ago )     #=> "about 3 hours"
    
26
Doug R

chronic_duration analyse le temps numérique en lisible et vice versa

7
usr

Si vous souhaitez afficher des durées significatives comprises entre quelques secondes et quelques jours, une alternative serait (car elle ne doit pas forcément donner les meilleurs résultats):

def human_duration(secs, significant_only = true)
  n = secs.round
  parts = [60, 60, 24, 0].map{|d| next n if d.zero?; n, r = n.divmod d; r}.
    reverse.Zip(%w(d h m s)).drop_while{|n, u| n.zero? }
  if significant_only
    parts = parts[0..1] # no rounding, sorry
    parts << '0' if parts.empty?
  end
  parts.flatten.join
end
start = Time.now
# perform job
puts "Elapsed time: #{human_duration(Time.now - start)}"

human_duration(0.3) == '0'
human_duration(0.5) == '1s'
human_duration(60) == '1m0s'
human_duration(4200) == '1h10m'
human_duration(3600*24) == '1d0h'
human_duration(3600*24 + 3*60*60) == '1d3h'
human_duration(3600*24 + 3*60*60 + 59*60) == '1d3h' # simple code, doesn't round
human_duration(3600*24 + 3*60*60 + 59*60, false) == '1d3h59m0s'

Sinon, vous ne voudrez peut-être que supprimer la partie secondes lorsque ce n'est pas grave (démontrant également une autre approche):

def human_duration(duration_in_seconds)
  n = duration_in_seconds.round
  parts = []
  [60, 60, 24].each{|d| n, r = n.divmod d; parts << r; break if n.zero?}
  parts << n unless n.zero?
  pairs = parts.reverse.Zip(%w(d h m s)[-parts.size..-1])
  pairs.pop if pairs.size > 2 # do not report seconds when irrelevant
  pairs.flatten.join
end

J'espère que cela pourra aider.

1
rosenfeld

Rails a une DateHelper pour les vues. Si ce n'est pas exactement ce que vous voulez, vous devrez peut-être écrire le vôtre.

@Mladen Jablanović a une réponse avec un bon exemple de code. Toutefois, si vous souhaitez continuer à personnaliser un exemple de méthode humanize, cela pourrait être un bon point de départ.

def humanized_array_secs(sec)
  [[60, 'minutes '], [60, 'hours '], [24, 'days ']].inject([[sec, 'seconds']]) do |ary, (count, next_name)|
    div, prev_name = ary.pop

    quot, remain = div.divmod(count)
    ary.Push([remain, prev_name])
    ary.Push([quot, next_name])
    ary
  end.reverse
end

Cela vous donne un tableau de valeurs et de noms d'unités que vous pouvez manipuler.

Si le premier élément est différent de zéro, c'est le nombre de jours. Vous voudrez peut-être écrire du code pour gérer plusieurs jours, par exemple afficher des semaines, des mois et des années. Sinon, coupez les valeurs 0 en tête et prenez les deux suivantes.

def humanized_secs(sec)
  return 'now' if 1 > sec

  humanized_array = humanized_array_secs(sec.to_i)
  days = humanized_array[-1][0]
  case
    when 366 <= days
      "#{days / 365} years"
    when 31 <= days
      "#{days / 31} months"
    when 7 <= days
      "#{days / 7} weeks"
    else
      while humanized_array.any? && (0 == humanized_array[-1][0])
        humanized_array.pop
      end
      humanized_array.reverse[0..1].flatten.join
  end
end

Le code trouve même une utilisation pour une instruction Ruby while.

0
Marlin Pierce

Il y a un problème avec distance_of_time_in_words si vous allez y passer 1 heure 30 min ça va revenir environ 2 heures

Ajoutez simplement dans helper:

 PERIODS = {
   'day' => 86400,
   'hour' => 3600,
   'minute' => 60
   }


def formatted_time(total)
  return 'now' if total.zero?

  PERIODS.map do |name, span|
    next if span > total
    amount, total = total.divmod(span)
    pluralize(amount, name)
  end.compact.to_sentence
end

En gros, il suffit de transmettre vos données en quelques secondes.

0
Denis