Можете ли вы обойти объект .NET TimeSpan?

Можете ли вы обойти объект .NET TimeSpan?

У меня есть значение Timespan: 00:00:00.6193789

Есть ли простой способ сохранить его как объект TimeSpan, но округлить его до чего-то вроде
00:00:00.62?


person BuddyJoe    schedule 03.12.2008    source источник
comment
Просто предупреждение другим. Я собирался использовать TimeSpan как общедоступную собственность бизнес-объекта. Не понимал, что TimeSpan не является типом данных XmlSerializable. Обсуждение проблемы: devnewsgroups.net/group/microsoft.public .dotnet.framework/   -  person BuddyJoe    schedule 04.12.2008


Ответы (10)


Извините, ребята, но и вопрос, и популярный ответ до сих пор неверны :-)

Вопрос неправильный, потому что Тиндалл запрашивает способ округлить, но показывает пример усечения.

Ответ Уилла Дина неверен, поскольку он также касается усечения, а не округления. (Полагаю, можно утверждать, что ответ правильный на один из двух вопросов, но давайте пока оставим философию в стороне...)

Вот простой способ округления:

int precision = 2; // Specify how many digits past the decimal point
TimeSpan t1 = new TimeSpan(19365678); // sample input value

const int TIMESPAN_SIZE = 7; // it always has seven digits
// convert the digitsToShow into a rounding/truncating mask
int factor = (int)Math.Pow(10,(TIMESPAN_SIZE - precision));

Console.WriteLine("Input: " + t1);
TimeSpan truncatedTimeSpan = new TimeSpan(t1.Ticks - (t1.Ticks % factor));
Console.WriteLine("Truncated: " + truncatedTimeSpan);
TimeSpan roundedTimeSpan =
    new TimeSpan(((long)Math.Round((1.0*t1.Ticks/factor))*factor));
Console.WriteLine("Rounded: " + roundedTimeSpan);

С входным значением и количеством цифр в образце кода это результат:

Input: 00:00:01.9365678
Truncated: 00:00:01.9300000
Rounded: 00:00:01.9400000

Измените точность с 2 цифр на 5 цифр и вместо этого получите следующее:

Input: 00:00:01.9365678
Truncated: 00:00:01.9365600
Rounded: 00:00:01.9365700

И даже измените его на 0, чтобы получить такой результат:

Input: 00:00:01.9365678
Truncated: 00:00:01
Rounded: 00:00:02

Наконец, если вы хотите чуть больше контроля над выводом, добавьте немного форматирования. Вот один пример, показывающий, что вы можете отделить точность от количества отображаемых цифр. Точность снова установлена ​​на 2, но отображаются 3 цифры, как указано в последнем аргументе строки управления форматированием:

Console.WriteLine("Rounded/formatted: " + 
  string.Format("{0:00}:{1:00}:{2:00}.{3:000}",
      roundedTimeSpan.Hours, roundedTimeSpan.Minutes,
      roundedTimeSpan.Seconds, roundedTimeSpan.Milliseconds));
// Input: 00:00:01.9365678
// Truncated: 00:00:01.9300000
// Rounded: 00:00:01.9400000
// Rounded/formatted: 00:00:01.940

ОБНОВЛЕНИЕ 2010.01.06: готовое решение

Приведенный выше материал полезен, если вы ищете идеи; С тех пор у меня было время реализовать пакетное решение для тех, кто ищет готовый к использованию код.

Обратите внимание, что это раскомментированный код. Полностью прокомментированная версия с комментариями XML-doc будет доступна в моей библиотеке с открытым исходным кодом к концу квартала. . Хотя я не решался публиковать его «сырым» в таком виде, я полагаю, что он все же может быть полезен заинтересованным читателям.

Этот код улучшает мой код выше, который, хотя и округлен, по-прежнему показывает 7 разрядов, дополненных нулями. Эта готовая версия округляется и обрезается до указанного количества цифр.

Вот пример вызова:

Console.Write(new RoundedTimeSpan(19365678, 2).ToString());
// Result = 00:00:01.94

А вот и полный файл RoundedTimeSpan.cs:

using System;

namespace CleanCode.Data
{
    public struct RoundedTimeSpan
    {

        private const int TIMESPAN_SIZE = 7; // it always has seven digits

        private TimeSpan roundedTimeSpan;
        private int precision;

        public RoundedTimeSpan(long ticks, int precision)
        {
            if (precision < 0) { throw new ArgumentException("precision must be non-negative"); }
            this.precision = precision;
            int factor = (int)System.Math.Pow(10, (TIMESPAN_SIZE - precision));

            // This is only valid for rounding milliseconds-will *not* work on secs/mins/hrs!
            roundedTimeSpan = new TimeSpan(((long)System.Math.Round((1.0 * ticks / factor)) * factor));
        }

        public TimeSpan TimeSpan { get { return roundedTimeSpan; } }

        public override string ToString()
        {
            return ToString(precision);
        }

        public string ToString(int length)
        { // this method revised 2010.01.31
            int digitsToStrip = TIMESPAN_SIZE - length;
            string s = roundedTimeSpan.ToString();
            if (!s.Contains(".") && length == 0) { return s; }
            if (!s.Contains(".")) { s += "." + new string('0', TIMESPAN_SIZE); }
            int subLength = s.Length - digitsToStrip;
            return subLength < 0 ? "" : subLength > s.Length ? s : s.Substring(0, subLength);
        }
    }
}

ОБНОВЛЕНИЕ 2010.02.01: теперь доступно пакетное решение

Я только вчера выпустил новую версию своих библиотек с открытым исходным кодом, раньше, чем ожидалось, включая RoundedTimeSpan, о котором я говорил выше. Код находится здесь; для API начните здесь, затем перейдите к RoundedTimeSpan в пространстве имен CleanCode.Data. Библиотека CleanCode.DLL включает показанный выше код, но предоставляет его в готовом пакете. Обратите внимание, что я сделал небольшое улучшение в методе ToString(int) выше, так как я опубликовал его 2010.01.06.

person Michael Sorens    schedule 05.01.2010
comment
Это перебор. Глядя на другие ответы, ни один из них не так прост, как может быть. См. мой ответ - person ToolmakerSteve; 28.09.2016
comment
более новый ответ NetMage также хорош, если вам нужны кратные произвольному промежутку времени. Например. иногда вам нужны секунды, иногда минуты, иногда миллисекунды. - person ToolmakerSteve; 05.08.2017

TimeSpan — это не более чем оболочка для члена «Ticks». Довольно легко создать новый TimeSpan из округленной версии Ticks другого TimeSpan.

TimeSpan t1 = new TimeSpan(2345678);
Console.WriteLine(t1);
TimeSpan t2 = new TimeSpan(t1.Ticks - (t1.Ticks % 100000));
Console.WriteLine(t2);

Дает:

00:00:00.2345678
00:00:00.2300000
person Will Dean    schedule 03.12.2008
comment
Ответ усекается (и, честно говоря, вопрос тоже). Если вместо этого вы хотите округлить, некоторые другие ответы делают это, но ни один из них не так прост, как может быть. См. мой ответ. - person ToolmakerSteve; 28.09.2016
comment
более новый ответ NetMage также хорош, если вам нужны кратные произвольному промежутку времени. Например. иногда вам нужны секунды, иногда минуты, иногда миллисекунды. - person ToolmakerSteve; 05.08.2017

Самый простой однострочный вариант, если вы округляете до целых секунд:

public static TimeSpan RoundSeconds( TimeSpan span ) {
    return TimeSpan.FromSeconds( Math.Round( span.TotalSeconds ) );
}

Чтобы округлить до трех цифр (например, до десятых, сотых долей секунды или миллисекунд):

public static TimeSpan RoundSeconds( TimeSpan span, int nDigits ) {
    // TimeSpan.FromSeconds rounds to nearest millisecond, so nDigits should be 3 or less - won't get good answer beyond 3 digits.
    Debug.Assert( nDigits <= 3 );
    return TimeSpan.FromSeconds( Math.Round( span.TotalSeconds, nDigits ) );
}

Для более чем 3 цифр это немного сложнее, но все же однострочник. Это также можно использовать для 3 или менее цифр - это замена версии, показанной выше:

public static TimeSpan RoundSeconds( TimeSpan span, int nDigits ) {
    return TimeSpan.FromTicks( (long)( Math.Round( span.TotalSeconds, nDigits ) * TimeSpan.TicksPerSecond) );
}

Если вам нужна строка (согласно комментарию, этот метод работает только до 7 цифр):

public static string RoundSecondsAsString( TimeSpan span, int nDigits ) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < nDigits; i++)
        sb.Append( "f" );
    return span.ToString( @"hh\:mm\:ss\." + sb );
}

Для времени дня в виде часов и минут с округлением:

public static TimeSpan RoundMinutes(TimeSpan span)
    {
        return TimeSpan.FromMinutes(Math.Round(span.TotalMinutes));
    }

Если у вас есть DateTime и вы хотите извлечь время дня, округленное:

DateTime dt = DateTime.Now();
TimeSpan hhmm = RoundMinutes(dt.TimeOfDay);

Чтобы отобразить округленное 24-часовое время:

string hhmmStr = RoundMinutes(dt.TimeOfDay).ToString(@"hh\:mm");

Чтобы отобразить время суток в текущей культуре:

string hhmmStr = new DateTime().Add(RoundMinutes(dt.TimeOfDay)).ToShortTimeString();

Кредиты:

Ответ cc1960 показывает использование FromSeconds, но он округлил до целых секунд. Мой ответ обобщается до указанного количества цифр.

Ответ Эда предлагает использовать строку формата и включает ссылку на документ форматирования.

Крис Марисик показывает, как применить ToShortTimeString к TimeSpan (сначала преобразовав в DateTime).


Чтобы округлить до кратности какой-либо другой единицы, например 1/30 секунды:

    // Rounds span to multiple of "unitInSeconds".
    // NOTE: This will be close to the requested multiple,
    // but is not exact when unit cannot be exactly represented by a double.
    // e.g. "unitInSeconds = 1/30" isn't EXACTLY 1/30,
    // so the returned value won't be exactly a multiple of 1/30.
    public static double RoundMultipleAsSeconds( TimeSpan span, double unitInSeconds )
    {
        return unitInSeconds * Math.Round( span.TotalSeconds / unitInSeconds );
    }

    public static TimeSpan RoundMultipleAsTimeSpan( TimeSpan span, double unitInSeconds )
    {
        return TimeSpan.FromTicks( (long)(RoundMultipleAsSeconds( span, unitInSeconds ) * TimeSpan.TicksPerSecond) );

        // IF USE THIS: TimeSpan.FromSeconds rounds the result to nearest millisecond.
        //return TimeSpan.FromSeconds( RoundMultipleAsSeconds( span, unitInSeconds ) );
    }

    // Rounds "span / n".
    // NOTE: This version might be a hair closer in some cases,
    // but probably not enough to matter, and can only represent units that are "1 / N" seconds.
    public static double RoundOneOverNAsSeconds( TimeSpan span, double n )
    {
        return Math.Round( span.TotalSeconds * n ) / n;
    }

    public static TimeSpan RoundOneOverNAsTimeSpan( TimeSpan span, double n )
    {
        return TimeSpan.FromTicks( (long)(RoundOneOverNAsSeconds( span, n ) * TimeSpan.TicksPerSecond) );

        // IF USE THIS: TimeSpan.FromSeconds rounds the result to nearest millisecond.
        //return TimeSpan.FromSeconds( RoundOneOverNAsSeconds( span, n ) );
    }

Чтобы использовать один из них для округления до кратных 1/30 секунды:

    private void Test()
    {
        long ticks = (long) (987.654321 * TimeSpan.TicksPerSecond);
        TimeSpan span = TimeSpan.FromTicks( ticks );
        TestRound( span, 30 );
        TestRound( TimeSpan.FromSeconds( 987 ), 30 );
    }

    private static void TestRound(TimeSpan span, int n)
    {
        var answer1 = RoundMultipleAsSeconds( span, 1.0 / n );
        var answer2 = RoundMultipleAsTimeSpan( span, 1.0 / n );
        var answer3 = RoundOneOverNAsSeconds( span, n );
        var answer4 = RoundOneOverNAsTimeSpan( span, n );
    }

Результаты, просмотренные в отладчике:

// for 987.654321 seconds:
    answer1 987.66666666666663  double
    answer2 {00:16:27.6666666}  System.TimeSpan
    answer3 987.66666666666663  double
    answer4 {00:16:27.6666666}  System.TimeSpan

// for 987 seconds:
    answer1 987 double
    answer2 {00:16:27}  System.TimeSpan
    answer3 987 double
    answer4 {00:16:27}  System.TimeSpan
person ToolmakerSteve    schedule 28.09.2016
comment
Мне это нравится, но мне также нравятся методы расширения: public static TimeSpan RoundSeconds(this TimeSpan span, int nDigits = 0) - person NetMage; 17.12.2016
comment
более новый ответ NetMage хорош, если вам нужны кратные произвольному промежутку времени. Например. иногда вам нужны секунды, иногда минуты, иногда миллисекунды. - person ToolmakerSteve; 05.08.2017
comment
Для RoundSecondsAsString: помните, что максимальное количество цифр (f), которое вы можете добавить, равно 7! Также в конце будет потерянная десятичная точка, если nDigits == 0 - person marsze; 27.12.2018
comment
Это здорово; Я упустил тот факт, что свойства TimeSpan.Total{Units} включают нецелочисленную часть, хотя это имеет смысл. (Я думал, что это больше похоже на свойство DateTime Day или Minute.) - person Gyromite; 18.03.2020

Учитывая некоторые комментарии об округлении до секунд, я подумал, что округление до любого TimeSpan было бы неплохо:

public static TimeSpan Round(this TimeSpan ts, TimeSpan rnd) {
    if (rnd == TimeSpan.Zero)
        return ts;
    else {
        var rndTicks = rnd.Ticks;
        var ansTicks = ts.Ticks + Math.Sign(ts.Ticks) * rndTicks / 2;
        return TimeSpan.FromTicks(ansTicks - ansTicks % rndTicks);
    }
}
public static TimeSpan Round(this TimeSpan ts) => ts.Round(TimeSpan.FromSeconds(1));

Учитывая потенциальные неточности округления до тиков при работе с дробными единицами (согласно @ToolmakerSteve), я добавляю опцию дробного округления, когда вам нужна более высокая точность и округляются до долей секунды компьютера:

public static TimeSpan RoundToFraction(this TimeSpan ts, long num, long den) => (den == 0.0) ? TimeSpan.Zero : TimeSpan.FromTicks((long)Math.Round(Math.Round((double)ts.Ticks * (double)den / num / TimeSpan.TicksPerSecond) * (double)num / den * TimeSpan.TicksPerSecond));
public static TimeSpan RoundToFraction(this TimeSpan ts, long den) => (den == 0.0) ? TimeSpan.Zero : TimeSpan.FromTicks((long)(Math.Round((double)ts.Ticks * den / TimeSpan.TicksPerSecond) / den * TimeSpan.TicksPerSecond));
person NetMage    schedule 16.12.2016
comment
Мне это нравится (так проголосовал). И он будет работать правильно для подавляющего большинства ситуаций, которые будут использовать люди. FWIW, имейте в виду небольшое ограничение: исправлять только в том случае, если желаемый rnd (делитель) выражается как целое число тиков. Например, если кто-то хочет округлить большое количество тиков до 30-х долей секунды, не спрашивайте меня, почему :D, это не поможет. К счастью, все распространенные случаи работают нормально: ЕСЛИ одна секунда равна степени 10 тиков (например, 100 000 тиков = 1 секунде), тогда работают 1/10 секунды, 1/100 секунды и т. д. а также 2/10 секунды, 2/100 секунды, .. и 5/10 секунды, ... - person ToolmakerSteve; 16.04.2017
comment
Хороший вопрос, но это ничем не отличается от большинства других операций с плавающей запятой в программировании. OTOH, трудно создать 1/30 секунды TimeSpan, поскольку Microsoft не реализовала FromTotal* варианты фабричных функций, поэтому, надеюсь, вам нужно знать, что вы делаете, чтобы добраться туда. - person NetMage; 20.04.2017
comment
Наоборот, оно отличается следующим образом: ваше решение вводит ограничение, которого нет в исходных данных. Вы ввели это ограничение, используя делитель rnd, который представляет собой целое количество тиков. Результат может быть заметно неверным, если кому-то нужен делитель, который не может быть точно выражен как целое число делений. Это большая ошибка, чем ошибка округления с плавающей запятой, которая самое большее приведет к отклонению ответа на 1. Любой, кто использует это, должен понимать это ограничение. - person ToolmakerSteve; 05.08.2017
comment
.. на практике это имеет значение только для крошечных значений rnd. Таким образом, ошибка в 1/30 секунды не имеет значения практически для любого использования. Чем меньше становится rnd, тем больше это может стать проблемой. Я просто хотел отметить тот факт, что здесь есть потенциальная неточность, которая происходит из-за того, что TimeSpan не является значением с плавающей запятой, поэтому становится более неточным по мере того, как значения становятся меньше. (Решением в таких случаях было бы не пытаться представить rnd как TimeSpan; вместо этого использовать количество секунд с плавающей запятой.) - person ToolmakerSteve; 05.08.2017
comment
.. или еще лучше, введите число, на которое хотите разделить. Я имею в виду, если вы хотите получить результат за 30 долей секунды, возьмите 30 в качестве второго параметра вместо 1/30. [Опять же, позвольте мне подчеркнуть, что я проголосовал за ответ как есть; Я делаю незначительные замечания, которые не будут иметь значения для большинства людей, ищущих решение. Это очень полезный ответ.] - person ToolmakerSteve; 05.08.2017
comment
Поскольку наименьшая единица времени, выражаемая в TimeSpan, составляет один тик, не означает ли это, что вы не можете равномерно округлить TimeSpan до 1/30 секунды, независимо от того, как вы выполняете математику? - person NetMage; 07.08.2017
comment
Я не пояснил, в чем проблема. Ваш подход дает заметно неправильный ответ для единиц 1/30 секунды для некоторых входных значений. Тем более, что входы становятся больше. Это отличается от округления, которое дает максимально близкий ответ. Разницу можно увидеть, запустив мой ответ и ваш ответ для множества разных случайных входных данных и увидев, где они отличаются и насколько. [Различия обычно небольшие, а часто и вовсе отсутствуют, но они есть. Это неправильная математика.] - person ToolmakerSteve; 30.12.2018
comment
@ToolmakerSteve Какой ваш ответ я могу использовать для округления до 1/30 секунды? - person NetMage; 03.01.2019
comment
@ToolmakerSteve Думаю, я вижу часть проблемы в том, что TimeSpan.FromSeconds указано с точностью до миллисекунды (!). - person NetMage; 03.01.2019
comment
извините, я забыл, что ответ, который я разместил здесь, работает только с цифрами, поэтому 1/10, 1/100 и т. д. Я добавил пример округления до указанной доли секунды. Как вы упомянули, во многих случаях результат не может быть ТОЧНО кратным 1/30 секунды, поскольку значения PC float или double не точно представляют 1/30 секунды, но он будет максимально близким, особенно метод RoundOneOverN. - person ToolmakerSteve; 04.01.2019
comment
@ToolmakerSteve Я бы посоветовал не использовать FromSeconds - он точен только до миллисекунды. Я написал свой собственный, который преобразуется в тики, а затем в TimeSpan. - person NetMage; 04.01.2019
comment
Да, я только что сам это обнаружил. Смотрите исправленный ответ. - person ToolmakerSteve; 04.01.2019
comment
После дальнейшего изучения: поскольку вы используете FromTicks вместо FromSeconds, ваш ответ, вероятно, работает нормально, за исключением случаев, когда ts ОЧЕНЬ большой, возможно, что минимальная неизбежная неточность в rndTicks увеличивается с помощью ansTicks % rndticks - вот почему я не рекомендую конвертировать rnd в галочки перед выполнением округления. OTOH, о крошечной возможной неточности не стоит беспокоиться - она ​​вообще может произойти только тогда, когда ts ОЧЕНЬ велико. - person ToolmakerSteve; 04.01.2019
comment
@ToolmakerSteve Некоторые тесты, которые я провел с округлением 1/30 с 0-30 секундами, показывают, что кажется проблематичным (для меня), но, возможно, это просто ограничения округления до тиков. - person NetMage; 04.01.2019
comment
@ToolmakerSteve Я добавил некоторые функции дробного округления, похожие на ваши, но все еще основанные на FromTicks. - person NetMage; 04.01.2019
comment
Не работает для отрицательных промежутков времени. Я хотел использовать это для округления до ближайшего числа дней, но обнаружил, что TimeSpan.FromDays(-4).Round(TimeSpan.FromDays(1)) приводит к -3 дням. - person Gyromite; 17.03.2020
comment
@Gyromite Обновлено для обработки отрицательных TimeSpan. - person NetMage; 17.03.2020

Не уверен насчет TimeSpan, но вы можете проверить этот пост на DateTimes:
http://mikeinmadison.wordpress.com/2008/03/12/datetimeround/

person Joel Coehoorn    schedule 03.12.2008

Вот хороший метод расширения:

    public static TimeSpan RoundToSeconds(this TimeSpan timespan, int seconds = 1)
    {
        long offset = (timespan.Ticks >= 0) ? TimeSpan.TicksPerSecond / 2 : TimeSpan.TicksPerSecond / -2;
        return TimeSpan.FromTicks((timespan.Ticks + offset) / TimeSpan.TicksPerSecond * TimeSpan.TicksPerSecond);
    }

И вот несколько примеров:

DateTime dt1 = DateTime.Now.RoundToSeconds();  // round to full seconds
DateTime dt2 = DateTime.Now.RoundToSeconds(5 * 60);  // round to full 5 minutes
person Gewalius    schedule 19.02.2016

Мое решение:

    static TimeSpan RoundToSec(TimeSpan ts)
    {
        return TimeSpan.FromSeconds((int)(ts.TotalSeconds));
    }
person cc1960    schedule 09.06.2016

Еще один способ округлить миллисекунды до ближайшей секунды.

private const long TicksPer1000Milliseconds = 1000 * TimeSpan.TicksPerMillisecond;

// Round milliseconds to nearest second
// To round up, add the sub-second ticks required to reach the next second
// To round down, subtract the sub-second ticks
elapsedTime = new TimeSpan(elapsedTime.Ticks + (elapsedTime.Milliseconds >= 500 ? TicksPer1000Milliseconds - (elapsedTime.Ticks % TicksPer1000Milliseconds) : -(elapsedTime.Ticks % TicksPer1000Milliseconds)));
person Buzz    schedule 26.06.2014
comment
Вместо TicksPer1000Milliseconds я бы назвал это TicksPerSeconds. - person Munk; 21.05.2019

Метод расширения, если вам нужно вместо этого работать с DateTime, но вы все равно хотите округлить время. В моем случае я хотел округлить до минуты.

public static DateTime RoundToMinute(this DateTime date)
    {
        return DateTime.MinValue.AddMinutes(Math.Round((date - DateTime.MinValue).TotalMinutes));
    }
person Math M.    schedule 30.06.2018
comment
Если вы используете 01 января 2021 г., 23:59:50, что вы ожидаете в качестве вывода? Если мое предположение верно, вы получите 01 января 2021 г., 00:00:00, что неверно почти на 24 часа. Здесь не имеет смысла смешивать время и дату. - person Tobias Knauss; 25.02.2021
comment
Вы правы, произошла ошибка. Я исправил это. В моем случае очень нужно было округлить момент по времени, включая дату до минуты. Согласен, что когда не надо, то проще работать только со временем. - person Math M.; 26.02.2021

person    schedule
comment
Вот что я делаю. На самом деле должен быть более простой способ, который не связан с галочками напрямую. - person gillonba; 04.12.2008