Calcule el tiempo relativo en C #

votos
1k

Dado un DateTimevalor específico , ¿cómo puedo mostrar la hora relativa, como:

  • Hace 2 horas
  • Hace 3 días
  • hace un mes
Publicado el 01/08/2008 a las 00:55
fuente por usuario
En otros idiomas...                            


37 respuestas

votos
318

Así es como lo hago

var ts = new TimeSpan(DateTime.UtcNow.Ticks - dt.Ticks);
double delta = Math.Abs(ts.TotalSeconds);

if (delta < 60)
{
  return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";
}
if (delta < 120)
{
  return "a minute ago";
}
if (delta < 2700) // 45 * 60
{
  return ts.Minutes + " minutes ago";
}
if (delta < 5400) // 90 * 60
{
  return "an hour ago";
}
if (delta < 86400) // 24 * 60 * 60
{
  return ts.Hours + " hours ago";
}
if (delta < 172800) // 48 * 60 * 60
{
  return "yesterday";
}
if (delta < 2592000) // 30 * 24 * 60 * 60
{
  return ts.Days + " days ago";
}
if (delta < 31104000) // 12 * 30 * 24 * 60 * 60
{
  int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  return months <= 1 ? "one month ago" : months + " months ago";
}
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
return years <= 1 ? "one year ago" : years + " years ago";

Sugerencias? ¿Comentarios? Formas de mejorar este algoritmo?

Respondida el 01/08/2008 a las 00:56
fuente por usuario

votos
28

@jeff

En mi humilde opinión parece un poco largo. Sin embargo, parece un poco más robusto con soporte para "ayer" y "años". Pero, en mi experiencia, cuando se usa esto, es más probable que la persona vea el contenido en los primeros 30 días. Solo la gente realmente hardcore viene después de eso. Es por eso que generalmente elijo mantener esto corto y simple.

Este es el método que estoy usando actualmente en uno de mis sitios web. Esto solo devuelve un día, hora y tiempo relativos. Y luego el usuario tiene que dar un "clic" en la salida.

public static string ToLongString(this TimeSpan time)
{
    string output = String.Empty;

    if (time.Days > 0)
        output += time.Days + " days ";

    if ((time.Days == 0 || time.Days == 1) && time.Hours > 0)
        output += time.Hours + " hr ";

    if (time.Days == 0 && time.Minutes > 0)
        output += time.Minutes + " min ";

    if (output.Length == 0)
        output += time.Seconds + " sec";

    return output.Trim();
}
Respondida el 01/08/2008 a las 13:17
fuente por usuario

votos
875

Jeff, tu código es bueno, pero podría ser más claro con constantes (como se sugiere en Code Complete).

const int SECOND = 1;
const int MINUTE = 60 * SECOND;
const int HOUR = 60 * MINUTE;
const int DAY = 24 * HOUR;
const int MONTH = 30 * DAY;

var ts = new TimeSpan(DateTime.UtcNow.Ticks - yourDate.Ticks);
double delta = Math.Abs(ts.TotalSeconds);

if (delta < 1 * MINUTE)
  return ts.Seconds == 1 ? "one second ago" : ts.Seconds + " seconds ago";

if (delta < 2 * MINUTE)
  return "a minute ago";

if (delta < 45 * MINUTE)
  return ts.Minutes + " minutes ago";

if (delta < 90 * MINUTE)
  return "an hour ago";

if (delta < 24 * HOUR)
  return ts.Hours + " hours ago";

if (delta < 48 * HOUR)
  return "yesterday";

if (delta < 30 * DAY)
  return ts.Days + " days ago";

if (delta < 12 * MONTH)
{
  int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
  return months <= 1 ? "one month ago" : months + " months ago";
}
else
{
  int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
  return years <= 1 ? "one year ago" : years + " years ago";
}
Respondida el 04/08/2008 a las 14:57
fuente por usuario

votos
14

Pensé que podría probar esto usando clases y polimorfismo. Tuve una iteración previa que utilizó subclasificación que terminó teniendo demasiada sobrecarga. Cambié a un modelo de objeto de propiedad público / delegado más flexible que es significativamente mejor. Mi código es un poco más preciso, ojalá pudiera encontrar una forma mejor de generar "meses atrás" que no pareciera demasiado ingeniosa.

Creo que aún me quedaré con la cascada de si-entonces de Jeff porque es menos código y es más simple (es definitivamente más fácil asegurarse de que funcione como se esperaba).

Para el siguiente código PrintRelativeTime.GetRelativeTimeMessage (TimeSpan atrás) devuelve el mensaje de tiempo relativo (por ejemplo, "ayer").

public class RelativeTimeRange : IComparable
{
    public TimeSpan UpperBound { get; set; }

    public delegate string RelativeTimeTextDelegate(TimeSpan timeDelta);

    public RelativeTimeTextDelegate MessageCreator { get; set; }

    public int CompareTo(object obj)
    {
        if (!(obj is RelativeTimeRange))
        {
            return 1;
        }
        // note that this sorts in reverse order to the way you'd expect, 
        // this saves having to reverse a list later
        return (obj as RelativeTimeRange).UpperBound.CompareTo(UpperBound);
    }
}

public class PrintRelativeTime
{
    private static List<RelativeTimeRange> timeRanges;

    static PrintRelativeTime()
    {
        timeRanges = new List<RelativeTimeRange>{
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromSeconds(1),
                MessageCreator = (delta) => 
                { return "one second ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromSeconds(60),
                MessageCreator = (delta) => 
                { return delta.Seconds + " seconds ago"; }

            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromMinutes(2),
                MessageCreator = (delta) => 
                { return "one minute ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromMinutes(60),
                MessageCreator = (delta) => 
                { return delta.Minutes + " minutes ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromHours(2),
                MessageCreator = (delta) => 
                { return "one hour ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromHours(24),
                MessageCreator = (delta) => 
                { return delta.Hours + " hours ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.FromDays(2),
                MessageCreator = (delta) => 
                { return "yesterday"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = DateTime.Now.Subtract(DateTime.Now.AddMonths(-1)),
                MessageCreator = (delta) => 
                { return delta.Days + " days ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = DateTime.Now.Subtract(DateTime.Now.AddMonths(-2)),
                MessageCreator = (delta) => 
                { return "one month ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = DateTime.Now.Subtract(DateTime.Now.AddYears(-1)),
                MessageCreator = (delta) => 
                { return (int)Math.Floor(delta.TotalDays / 30) + " months ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = DateTime.Now.Subtract(DateTime.Now.AddYears(-2)),
                MessageCreator = (delta) => 
                { return "one year ago"; }
            }, 
            new RelativeTimeRange
            {
                UpperBound = TimeSpan.MaxValue,
                MessageCreator = (delta) => 
                { return (int)Math.Floor(delta.TotalDays / 365.24D) + " years ago"; }
            }
        };

        timeRanges.Sort();
    }

    public static string GetRelativeTimeMessage(TimeSpan ago)
    {
        RelativeTimeRange postRelativeDateRange = timeRanges[0];

        foreach (var timeRange in timeRanges)
        {
            if (ago.CompareTo(timeRange.UpperBound) <= 0)
            {
                postRelativeDateRange = timeRange;
            }
        }

        return postRelativeDateRange.MessageCreator(ago);
    }
}
Respondida el 05/08/2008 a las 01:42
fuente por usuario

votos
84
public static string RelativeDate(DateTime theDate)
{
    Dictionary<long, string> thresholds = new Dictionary<long, string>();
    int minute = 60;
    int hour = 60 * minute;
    int day = 24 * hour;
    thresholds.Add(60, "{0} seconds ago");
    thresholds.Add(minute * 2, "a minute ago");
    thresholds.Add(45 * minute, "{0} minutes ago");
    thresholds.Add(120 * minute, "an hour ago");
    thresholds.Add(day, "{0} hours ago");
    thresholds.Add(day * 2, "yesterday");
    thresholds.Add(day * 30, "{0} days ago");
    thresholds.Add(day * 365, "{0} months ago");
    thresholds.Add(long.MaxValue, "{0} years ago");
    long since = (DateTime.Now.Ticks - theDate.Ticks) / 10000000;
    foreach (long threshold in thresholds.Keys) 
    {
        if (since < threshold) 
        {
            TimeSpan t = new TimeSpan((DateTime.Now.Ticks - theDate.Ticks));
            return string.Format(thresholds[threshold], (t.Days > 365 ? t.Days / 365 : (t.Days > 0 ? t.Days : (t.Hours > 0 ? t.Hours : (t.Minutes > 0 ? t.Minutes : (t.Seconds > 0 ? t.Seconds : 0))))).ToString());
        }
    }
    return "";
}

Prefiero esta versión por su concisión y su capacidad de agregar nuevos puntos de tilde. Esto podría ser encapsulado con una Latest()extensión de Timespan en lugar de ese largo 1 del trazador de líneas, pero en aras de la brevedad en la publicación, esto servirá. Esto corrige la hace una hora, hace 1 hora, proporcionando una hora hasta que hayan transcurrido 2 horas

Respondida el 14/08/2008 a las 06:43
fuente por usuario

votos
8

@Jeff

var ts = new TimeSpan (DateTime.UtcNow.Ticks - dt.Ticks);

Hacer una resta en DateTimedevuelve a de TimeSpantodos modos.

Entonces puedes hacer

(DateTime.UtcNow - dt).TotalSeconds

También me sorprende ver las constantes multiplicadas a mano y luego los comentarios agregados con las multiplicaciones. ¿Fue alguna optimización equivocada?

Respondida el 15/08/2008 a las 15:20
fuente por usuario

votos
11

Cuando conoces la zona horaria del espectador, puede ser más claro usar días calendario en la escala de días. No estoy familiarizado con las bibliotecas .NET, así que no sé cómo harías eso en C #, desafortunadamente.

En los sitios de consumidores, también podría ser más rápido en menos de un minuto. "Hace menos de un minuto" o "ahora mismo" podría ser suficiente.

Respondida el 15/08/2008 a las 23:42
fuente por usuario

votos
8

Puede reducir la carga del lado del servidor realizando esta lógica del lado del cliente. Ver fuente en algunas páginas de Digg para referencia. Tienen el servidor emitir un valor de tiempo de época que es procesado por Javascript. De esta forma, no es necesario administrar la zona horaria del usuario final. El nuevo código del lado del servidor sería algo así como:

public string GetRelativeTime(DateTime timeStamp)
{
    return string.Format("<script>printdate({0});</script>", timeStamp.ToFileTimeUtc());
}

Incluso podría agregar un bloque NOSCRIPT allí y simplemente realizar un ToString ().

Respondida el 17/08/2008 a las 16:56
fuente por usuario

votos
18

En PHP, lo hago de esta manera:

<?php
function timesince($original) {
    // array of time period chunks
    $chunks = array(
        array(60 * 60 * 24 * 365 , 'year'),
        array(60 * 60 * 24 * 30 , 'month'),
        array(60 * 60 * 24 * 7, 'week'),
        array(60 * 60 * 24 , 'day'),
        array(60 * 60 , 'hour'),
        array(60 , 'minute'),
    );

    $today = time(); /* Current unix time  */
    $since = $today - $original;

    if($since > 604800) {
    $print = date("M jS", $original);

    if($since > 31536000) {
        $print .= ", " . date("Y", $original);
    }

    return $print;
}

// $j saves performing the count function each time around the loop
for ($i = 0, $j = count($chunks); $i < $j; $i++) {

    $seconds = $chunks[$i][0];
    $name = $chunks[$i][1];

    // finding the biggest chunk (if the chunk fits, break)
    if (($count = floor($since / $seconds)) != 0) {
        break;
    }
}

$print = ($count == 1) ? '1 '.$name : "$count {$name}s";

return $print . " ago";

} ?>
Respondida el 20/08/2008 a las 18:26
fuente por usuario

votos
2

Seguramente una solución fácil para deshacerse del problema de '1 hora