Como arredondar o tempo para os X minutos mais próximos?

160

Existe uma função simples para arredondamento UP um DateTimedos mais próximos 15 minutos?

Por exemplo

2011-08-11 16:59 torna-se 2011-08-11 17:00

2011-08-11 17:00 permanece como 2011-08-11 17:00

2011-08-11 17:01 torna-se 2011-08-11 17:15

TimS
fonte

Respostas:

286
DateTime RoundUp(DateTime dt, TimeSpan d)
{
    return new DateTime((dt.Ticks + d.Ticks - 1) / d.Ticks * d.Ticks, dt.Kind);
}

Exemplo:

var dt1 = RoundUp(DateTime.Parse("2011-08-11 16:59"), TimeSpan.FromMinutes(15));
// dt1 == {11/08/2011 17:00:00}

var dt2 = RoundUp(DateTime.Parse("2011-08-11 17:00"), TimeSpan.FromMinutes(15));
// dt2 == {11/08/2011 17:00:00}

var dt3 = RoundUp(DateTime.Parse("2011-08-11 17:01"), TimeSpan.FromMinutes(15));
// dt3 == {11/08/2011 17:15:00}
dtb
fonte
13
Esta solução acabou de entrar na minha biblioteca de utilitários como um método de extensão.
precisa saber é o seguinte
1
Cuidado com os tempos de arredondamento próximos ao extremo superior. Isso pode causar o lançamento de uma exceção se os Ticks calculados forem maiores que DateTime.MaxValue.Ticks. Esteja seguro e use o mínimo de seu valor calculado e DateTime.MaxValue.Ticks.
precisa
4
Você não está perdendo informações do objeto DateTime com esse método? Como o tipo e o fuso horário, se houver algum?
Evren Kuzucuoglu 14/07
11
@ user14 .. O (+ d.Ticks - 1) garante que o arredondamento seja necessário, se necessário. O / e * estão arredondando. Exemplo 12 rodada para o lado 5: (12 + 5 - 1) = 16, 16/5 = 3 (porque é um tipo de dados inteiro), 3 * 5 = 15. tada :)
Diego Frehner
12
@dtb um pequeno aditamento, caso contrário ele é provavelmente um pouco incomodado: você precisa manter o tipo de data e hora ;-) DateTime RoundUp(DateTime dt, TimeSpan d) { return new DateTime(((dt.Ticks + d.Ticks - 1) / d.Ticks) * d.Ticks, dt.Kind); }
njy
107

Surgiu uma solução que não envolve multiplicar e dividir long números.

public static DateTime RoundUp(this DateTime dt, TimeSpan d)
{
    var modTicks = dt.Ticks % d.Ticks;
    var delta = modTicks != 0 ? d.Ticks - modTicks : 0;
    return new DateTime(dt.Ticks + delta, dt.Kind);
}

public static DateTime RoundDown(this DateTime dt, TimeSpan d)
{
    var delta = dt.Ticks % d.Ticks;
    return new DateTime(dt.Ticks - delta, dt.Kind);
}

public static DateTime RoundToNearest(this DateTime dt, TimeSpan d)
{
    var delta = dt.Ticks % d.Ticks;
    bool roundUp = delta > d.Ticks / 2;
    var offset = roundUp ? d.Ticks : 0;

    return new DateTime(dt.Ticks + offset - delta, dt.Kind);
}

Uso:

var date = new DateTime(2010, 02, 05, 10, 35, 25, 450); // 2010/02/05 10:35:25
var roundedUp = date.RoundUp(TimeSpan.FromMinutes(15)); // 2010/02/05 10:45:00
var roundedDown = date.RoundDown(TimeSpan.FromMinutes(15)); // 2010/02/05 10:30:00
var roundedToNearest = date.RoundToNearest(TimeSpan.FromMinutes(15)); // 2010/02/05 10:30:00
redent84
fonte
8
Eu tinha certeza de que isso seria mais rápido do que usar multiplicação e divisão, mas meus testes mostram que não. Em mais de 10000000 iterações, o método de módulo levou ~ 610ms na minha máquina, enquanto o método mult / div levou ~ 500ms. Eu acho que as FPUs tornam as preocupações do passado um problema. Aqui está o meu código de teste: pastie.org/8610460
viggity
1
Ótimo uso de extensões. Obrigado!
precisa saber é o seguinte
1
@Alovchin Obrigado. Eu atualizei a resposta. Criei esse ideone com seu código para mostrar a diferença: ideone.com/EVKFp5
redent84
1
Isso é muito velho, mas é a última %d.Ticksna RoundUpnecessário? d.Ticks - (dt.Ticks % d.Ticks))será necessariamente menor que d.Ticks, então a resposta deve ser a mesma correta?
Nate Diamond
1
Apenas apontando, o módulo é um requer uma operação de divisão na CPU. Mas concordo que é mais elegante do que usar a propriedade de desdobramento das divisões inteiras.
1628 Alex
19

se você precisar arredondar para um intervalo de tempo mais próximo (não acima), sugiro usar o seguinte

    static DateTime RoundToNearestInterval(DateTime dt, TimeSpan d)
    {
        int f=0;
        double m = (double)(dt.Ticks % d.Ticks) / d.Ticks;
        if (m >= 0.5)
            f=1;            
        return new DateTime(((dt.Ticks/ d.Ticks)+f) * d.Ticks);
    }
DevSal
fonte
Esta resposta não é arredondada corretamente. user1978424 tem a única publicação que mostra corretamente como arredondar para o intervalo mais próximo abaixo: (ironicamente votado para baixo porque a pergunta estava arredondando para CIMA) #
stitty
8
void Main()
{
    var date1 = new DateTime(2011, 8, 11, 16, 59, 00);
    date1.Round15().Dump();

    var date2 = new DateTime(2011, 8, 11, 17, 00, 02);
    date2.Round15().Dump();

    var date3 = new DateTime(2011, 8, 11, 17, 01, 23);
    date3.Round15().Dump();

    var date4 = new DateTime(2011, 8, 11, 17, 00, 00);
    date4.Round15().Dump();
}

public static class Extentions
{
    public static DateTime Round15(this DateTime value)
    {   
        var ticksIn15Mins = TimeSpan.FromMinutes(15).Ticks;

        return (value.Ticks % ticksIn15Mins == 0) ? value : new DateTime((value.Ticks / ticksIn15Mins + 1) * ticksIn15Mins);
    }
}

Resultados:

8/11/2011 5:00:00 PM
8/11/2011 5:15:00 PM
8/11/2011 5:15:00 PM
8/11/2011 5:00:00 PM
Vlad Bezden
fonte
3
2011-08-11 17:00:01fica truncado para2011-08-11 17:00:00
JYelton 11/11
1
@JYelton: Obrigado por apontar +1. Mudei meu código para acomodar isso.
Vlad Bezden
Fornecer seu formato de código Linqpad para fácil verificação economiza muito tempo. Muito fácil de usar.
24517 Adam Garner
6

Como odeio reinventar a roda, provavelmente seguiria esse algoritmo para arredondar um valor DateTime para um incremento especificado de tempo (Timespan):

  • Converta o DateTimevalor a ser arredondado em um valor decimal de ponto flutuante representando o número inteiro e fracionário de TimeSpanunidades.
  • Arredonde isso para um número inteiro, usando Math.Round().
  • Redimensione para ticks multiplicando o número inteiro arredondado pelo número de ticks na TimeSpanunidade.
  • Instancie um novo DateTimevalor a partir do número arredondado de ticks e devolva-o ao chamador.

Aqui está o código:

public static class DateTimeExtensions
{

    public static DateTime Round( this DateTime value , TimeSpan unit )
    {
        return Round( value , unit , default(MidpointRounding) ) ;
    }

    public static DateTime Round( this DateTime value , TimeSpan unit , MidpointRounding style )
    {
        if ( unit <= TimeSpan.Zero ) throw new ArgumentOutOfRangeException("unit" , "value must be positive") ;

        Decimal  units        = (decimal) value.Ticks / (decimal) unit.Ticks ;
        Decimal  roundedUnits = Math.Round( units , style ) ;
        long     roundedTicks = (long) roundedUnits * unit.Ticks ;
        DateTime instance     = new DateTime( roundedTicks ) ;

        return instance ;
    }

}
Nicholas Carey
fonte
Isso é bom código para arredondamento para o mais próximo DateTime , mas também quero a capacidade de rodada -se a um múltiplo de unit . Passar MidpointRounding.AwayFromZeropara Roundnão tem o efeito desejado. Você tem outra coisa em mente ao aceitar um MidpointRoundingargumento?
HappyNomad
2

Minha versão

DateTime newDateTimeObject = oldDateTimeObject.AddMinutes(15 - oldDateTimeObject.Minute % 15);

Como método, ele trava assim

public static DateTime GetNextQuarterHour(DateTime oldDateTimeObject)
{
    return oldDateTimeObject.AddMinutes(15 - oldDateTimeObject.Minute % 15);
}

e é chamado assim

DateTime thisIsNow = DateTime.Now;
DateTime nextQuarterHour = GetNextQuarterHour(thisIsNow);
soulflyman
fonte
isso não explica segundos
Alex Norcliffe
1

Elegante?

dt.AddSeconds(900 - (x.Minute * 60 + x.Second) % 900)
Olaf
fonte
1
Uma versão mais correta seria: x.AddSeconds (900 - (x.AddSeconds (-1) .Minuto * 60 + x.AddSeconds (-1) .Segundo)% 900) .AddSeconds (-1), que cuida de a condição "permanece".
Olaf
1

Cuidado: a fórmula acima está incorreta, ou seja, o seguinte:

DateTime RoundUp(DateTime dt, TimeSpan d)
{
    return new DateTime(((dt.Ticks + d.Ticks - 1) / d.Ticks) * d.Ticks);
}

deve ser reescrito como:

DateTime RoundUp(DateTime dt, TimeSpan d)
{
    return new DateTime(((dt.Ticks + d.Ticks/2) / d.Ticks) * d.Ticks);
}
user1978424
fonte
1
Discordo. Como a divisão inteira é / d.Ticksarredondada para o intervalo de 15 minutos mais próximo (vamos chamar esses "blocos"), adicionar apenas meio bloco não garante o arredondamento. Considere quando você tiver 4,25 blocos. Se você adicionar 0,5 blocos e testar quantos blocos inteiros você possui, você ainda terá apenas 4. Adicionar uma marca a menos que um bloco completo é a ação correta. Ele garante que você sempre avance para o próximo intervalo de blocos (antes de arredondar para baixo), mas impede que você se mova entre os blocos exatos. (IE, se você adicionou um bloco inteiro para 4,0 blocos, 5.0 seria arredondar para 5, quando quiser 4. 4.99 será 4.)
Brendan Moore
1

Uma solução mais detalhada, que utiliza módulo e evita cálculos desnecessários.

public static class DateTimeExtensions
{
    public static DateTime RoundUp(this DateTime dt, TimeSpan ts)
    {
        return Round(dt, ts, true);
    }

    public static DateTime RoundDown(this DateTime dt, TimeSpan ts)
    {
        return Round(dt, ts, false);
    }

    private static DateTime Round(DateTime dt, TimeSpan ts, bool up)
    {
        var remainder = dt.Ticks % ts.Ticks;
        if (remainder == 0)
        {
            return dt;
        }

        long delta;
        if (up)
        {
            delta = ts.Ticks - remainder;
        }
        else
        {
            delta = -remainder;
        }

        return dt.AddTicks(delta);
    }
}
Bo Sunesen
fonte
0

Esta é uma solução simples para arredondar para o minuto mais próximo. Ele preserva as informações TimeZone e Kind do DateTime. Ele pode ser modificado para atender às suas próprias necessidades (se você precisar arredondar para os 5 minutos mais próximos, etc.).

DateTime dbNowExact = DateTime.Now;
DateTime dbNowRound1 = (dbNowExact.Millisecond == 0 ? dbNowExact : dbNowExact.AddMilliseconds(1000 - dbNowExact.Millisecond));
DateTime dbNowRound2 = (dbNowRound1.Second == 0 ? dbNowRound1 : dbNowRound1.AddSeconds(60 - dbNowRound1.Second));
DateTime dbNow = dbNowRound2;
dodgy_coder
fonte
0

Você pode usar esse método, ele usa a data especificada para garantir a manutenção de qualquer tipo de globalização e data e hora especificado anteriormente no objeto data e hora.

const long LNG_OneMinuteInTicks = 600000000;
/// <summary>
/// Round the datetime to the nearest minute
/// </summary>
/// <param name = "dateTime"></param>
/// <param name = "numberMinutes">The number minute use to round the time to</param>
/// <returns></returns>        
public static DateTime Round(DateTime dateTime, int numberMinutes = 1)
{
    long roundedMinutesInTicks = LNG_OneMinuteInTicks * numberMinutes;
    long remainderTicks = dateTime.Ticks % roundedMinutesInTicks;
    if (remainderTicks < roundedMinutesInTicks / 2)
    {
        // round down
        return dateTime.AddTicks(-remainderTicks);
    }

    // round up
    return dateTime.AddTicks(roundedMinutesInTicks - remainderTicks);
}

Teste de violino líquido

Se você quiser usar o TimeSpan para arredondar, poderá usá-lo.

/// <summary>
/// Round the datetime
/// </summary>
/// <example>Round(dt, TimeSpan.FromMinutes(5)); => round the time to the nearest 5 minutes.</example>
/// <param name = "dateTime"></param>
/// <param name = "roundBy">The time use to round the time to</param>
/// <returns></returns>        
public static DateTime Round(DateTime dateTime, TimeSpan roundBy)
{            
    long remainderTicks = dateTime.Ticks % roundBy.Ticks;
    if (remainderTicks < roundBy.Ticks / 2)
    {
        // round down
        return dateTime.AddTicks(-remainderTicks);
    }

    // round up
    return dateTime.AddTicks(roundBy.Ticks - remainderTicks);
}

TimeSpan Fiddle

Du D.
fonte
O que acontece se você quiser arredondar para o sétimo minuto mais próximo var d = new DateTime(2019, 04, 15, 9, 40, 0, 0);// deve ser 9:42, mas nenhum desses métodos funciona assim?
DotnetShadow
Editar olhares como resposta @soulflyman iria produzir o resultado certo
DotnetShadow