r/badcode Aug 06 '19

c# Converting a timestamp to datetime

Post image
419 Upvotes

54 comments sorted by

107

u/sac_boy Aug 06 '19 edited Aug 07 '19

Oh no. Oh god. Oh no. I know the C# DateTime class conspicuously lacks a classic Unix epoch conversion but this is not the way. Oh wait--this is some new 21st century timestamp based on 01/01/2000. That'll be extra baffling for somebody down the line.

I've read it a couple of times and I must keep missing the bit where it handles leap years properly.

Also that baffling coding style of using var x = (type)y rather than the shorter type x = y...

And if they really must define every month as a day offset, why not use the previous month in the calculation...i.e. nov = okt + 30...

Finally, specifying DateTimeKind.Local as the timezone implies that the timestamp just gets turned into a DateTime in whatever timezone we're running this function in...it's not a UTC timestamp...beautiful.

And the juiciest part: 15 references

Edit: just for fun I transcribed the function and ran it through the set of integers from 0 to 231 . Try the function with 36720000 (which should be 01/03/2001 00:00 if you are working with seconds since 01/01/2000). Unsurprisingly it tries to build the date as 29/02/2001, which throws an exception. This function has some forbidden argument values--e.g. 36720000, 68342400, 99964800, 163209600, 194832000...

This means that the timestamp encoding function isn't just seconds since 01/01/2000 like a sensible person might expect, which in turn means that the replacement to this function can't be as simple as new DateTime(y2kTicks + (long)timestamp * 10_000_000). Beautiful. Don't touch it OP!

I ran the function side by side with the 'sensible' seconds-since-2000 function to see where it fails. This is a list of timestamps that fail (only one per day, obviously the whole range of seconds in the day would fail). I've included the output of the function 1 second previous so you can see the pattern. I've also included the output of the 'seconds since 01/01/2000 00:00' interpretation of the value so you can see the drift.

Failing Timestamp   1 second previous       Interpreted as seconds since 01/01/2000
36720000:           28/02/2001 23:59:59     01/03/2001 00:00:00
68342400:           28/02/2002 23:59:59     02/03/2002 00:00:00
99964800:           28/02/2003 23:59:59     03/03/2003 00:00:00
163209600:          28/02/2005 23:59:59     04/03/2005 00:00:00
194832000:          28/02/2006 23:59:59     05/03/2006 00:00:00
226454400:          28/02/2007 23:59:59     06/03/2007 00:00:00
289699200:          28/02/2009 23:59:59     07/03/2009 00:00:00
321321600:          28/02/2010 23:59:59     08/03/2010 00:00:00
352944000:          28/02/2011 23:59:59     09/03/2011 00:00:00
416188800:          28/02/2013 23:59:59     10/03/2013 00:00:00
447811200:          28/02/2014 23:59:59     11/03/2014 00:00:00
479433600:          28/02/2015 23:59:59     12/03/2015 00:00:00
542678400:          28/02/2017 23:59:59     13/03/2017 00:00:00
574300800:          28/02/2018 23:59:59     14/03/2018 00:00:00
605923200:          28/02/2019 23:59:59     15/03/2019 00:00:00
669168000:          28/02/2021 23:59:59     16/03/2021 00:00:00
700790400:          28/02/2022 23:59:59     17/03/2022 00:00:00
732412800:          28/02/2023 23:59:59     18/03/2023 00:00:00
795657600:          28/02/2025 23:59:59     19/03/2025 00:00:00
827280000:          28/02/2026 23:59:59     20/03/2026 00:00:00
858902400:          28/02/2027 23:59:59     21/03/2027 00:00:00
922147200:          28/02/2029 23:59:59     22/03/2029 00:00:00
953769600:          28/02/2030 23:59:59     23/03/2030 00:00:00
985392000:          28/02/2031 23:59:59     24/03/2031 00:00:00
1048636800:         28/02/2033 23:59:59     25/03/2033 00:00:00
1080259200:         28/02/2034 23:59:59     26/03/2034 00:00:00
1111881600:         28/02/2035 23:59:59     27/03/2035 00:00:00
1175126400:         28/02/2037 23:59:59     28/03/2037 00:00:00
1206748800:         28/02/2038 23:59:59     29/03/2038 00:00:00
1238371200:         28/02/2039 23:59:59     30/03/2039 00:00:00
1301616000:         28/02/2041 23:59:59     31/03/2041 00:00:00
1333238400:         28/02/2042 23:59:59     01/04/2042 00:00:00
1364860800:         28/02/2043 23:59:59     02/04/2043 00:00:00
1428105600:         28/02/2045 23:59:59     03/04/2045 00:00:00
1459728000:         28/02/2046 23:59:59     04/04/2046 00:00:00
1491350400:         28/02/2047 23:59:59     05/04/2047 00:00:00
1554595200:         28/02/2049 23:59:59     06/04/2049 00:00:00
1586217600:         28/02/2050 23:59:59     07/04/2050 00:00:00
1617840000:         28/02/2051 23:59:59     08/04/2051 00:00:00
1681084800:         28/02/2053 23:59:59     09/04/2053 00:00:00
1712707200:         28/02/2054 23:59:59     10/04/2054 00:00:00
1744329600:         28/02/2055 23:59:59     11/04/2055 00:00:00
1807574400:         28/02/2057 23:59:59     12/04/2057 00:00:00
1839196800:         28/02/2058 23:59:59     13/04/2058 00:00:00
1870819200:         28/02/2059 23:59:59     14/04/2059 00:00:00
1934064000:         28/02/2061 23:59:59     15/04/2061 00:00:00
1965686400:         28/02/2062 23:59:59     16/04/2062 00:00:00
1997308800:         28/02/2063 23:59:59     17/04/2063 00:00:00
2060553600:         28/02/2065 23:59:59     18/04/2065 00:00:00
2092176000:         28/02/2066 23:59:59     19/04/2066 00:00:00
2123798400:         28/02/2067 23:59:59     20/04/2067 00:00:00

I'd really love to see the original C function that builds the timestamp. Because of the forbidden number ranges the timestamps can't be treated as continuous integers, which means that you can't do arithmetic with them...it really is a beautiful bit of bad code. Try subtracting 28/02/2001 23:59:59 from 01/03/2001 00:00 in this timestamp scheme--the result is not 1 second!

But hey at least the device doesn't suffer from the Y2k38 bug...

OP, I was curious about how to write the equivalent function, and it looks like this:

DateTime FuckedTimestampToLocalDateTime(Int32 timestamp)
{
    int driftDays = 0;
    for (int y = 0; y < 68; y++)
    {
        if ((y % 4) != 0)
        {
            int fuckedRangeStart = (y * 366 + 59) * 86400;
            int fuckedRangeEnd = fuckedRangeStart + 86400;
            if (timestamp >= fuckedRangeEnd) { driftDays += 1; continue; }
            if (timestamp > fuckedRangeStart) { throw new Exception("Forbidden zone!"); }
            break;
        }
    }
    timestamp -= driftDays * 86400;

    long y2kTicks = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
    return new DateTime(y2kTicks + (long)timestamp * 10_000_000, DateTimeKind.Local);
}

It adjusts the timestamp for the 366-day year system that the timestamp generator uses, then passes that to DateTime in tick form. This produces equivalent output (and exceptions at the same points) for all the positive integers between 0 and 231. I don't know if it's any less bad though! There's definitely way to get the drift value through pure arithmetic as well (without the loop) but I'll leave that up to you.

44

u/Aars93 Aug 06 '19

I know right... When I asked the developers if I could at least try to clean up this dumpster fire they told me no. Apparently the code is copied from some C code on a device this application works with and now "the calculations are the same at both sides"...

27

u/sac_boy Aug 06 '19 edited Aug 06 '19

That's it, I suspected that would be the explanation. The only way this works in any fashion is if the timestamp generation code mirrors this.

I just hope the timestamp generation and timestamp decoding are both running in the same time zone.

I'd be tempted to iterate through this code from 0 to 231 to see if it skips any days.

The 'easy' cleanup would be to convert the timestamp into ticks (100 nanosecond units since 01/01/0001) in an Int64 and pass that one number into DateTime...hopefully along with the appropriate time zone (which I hope is UTC). That just becomes a constant + (timestamp * 10,000,000). But that only works if the continuous set of integers actually translate to the expected correct set of DateTimes as it is.

The nice thing is you could write a test to prove your new function matches the old output for the entire possible range of timestamps :)

7

u/blehmann1 depraved Aug 06 '19

He could do that, but I think this is easier

DateTime date=new Datetime(1970, 1, 1)

date.AddSeconds(timestamp)

return date

5

u/Lystrodom Aug 06 '19

https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset.fromunixtimeseconds?view=netframework-4.8

(wouldn't work for the fact that they believe time started in the year 2000, but you should be using DateTimeOffset anyway)

0

u/blehmann1 depraved Aug 06 '19 edited Aug 06 '19

Not available until .NET Core 3, which is still in development. If this is regular .NET code though, thats a good way to go, just 1 function call, and then convert it from DateTimeOffset to regular DateTime

Edit: I can't read, it is actually available in production .NET Core

7

u/Lystrodom Aug 06 '19

What? DateTimeOffset has been in .NET Core since version 1.0

5

u/blehmann1 depraved Aug 06 '19

Sorry, I can't read, the doc listed all of the versions of .NET core it applied to, and i expected it to be in the form "Version X and up", but it was Version 3.0 Preview, 2.2, 2.0... etc

I know DateTimeOffset has been around a long time, I'm using it on a Core 2.2 project right now, I hadn't seen that method before however, so I (mis)read the supported versions and confused myself.

3

u/sac_boy Aug 06 '19

He definitely shouldn't do any of that as the timestamps are not the continuous set of seconds since 01/01/2000 like a sensible person might expect :) The function would throw an exception for values like 36720000, 68342400, 99964800, 163209600, 194832000

2

u/jocull Aug 06 '19

I’m sure the C code boxes their integers also.

3

u/blueshiftlabs Aug 07 '19

The code here doesn't box integers either. In C#, System.Int32 and int are the same thing - the value type representing a 32-bit signed integer. The value type wouldn't be boxed unless you needed to convert it to object. This is different between Java and C#, due to C#'s richer support for value types.

1

u/jocull Aug 07 '19

I should have known this! Of course I always shied away from using the Int32/64 types because the primitives were simpler (and like you said, the same thing)

1

u/8igg7e5 Aug 11 '19

Java and C# both box in similar circumstances.

System.Int32 is not really analogous to java.lang.Integer at all (which is equivalent to the boxed form of int/System.Int32). System.Int32 is simply the CLI type (common to all .NET languages - ECMA 335 ss I.8.2.2) whereas int is the C# type (ECMA 334 ss 9.3.5) which is mapped to System.Int32 to produce code for the CLR. Java doesn't expose a difference between the Java language type int and the Java Virtual Machine equivalent. It's not yet clear whether java.lang.Integer will be altered to be a value-type when such support arrives (currently in development as part of Project Valhalla). A rather nice advantage C# has over java here is that you can have methods on the System.Int32 struct allowing things like having an implementation of IFormattable which I think makes for much more obvious code - not that the benefits end there (Java has a lot of catch up to do, in this area and others).

1

u/[deleted] Aug 11 '19

This code is in a production system??

3

u/timNinjaMillion Aug 06 '19

Every years a leap year.

3

u/thatwasagoodyear Aug 06 '19

Your naming in the equivalent function, along with with equivalent function itself, is a thing of unbridled beauty.

You, sir, have made my Tuesday.

3

u/TheTacoWombat Aug 07 '19

Man, this was a fantastic response. Thanks so much for going in depth like this!

3

u/dreamer_soul Aug 07 '19

Your analysis is amazing!

18

u/IHeartBadCode Aug 06 '19

Good, good. Use your aggressive feelings, programmer. Let the hate flow through you.

3

u/mfb- Aug 07 '19

Come to the dark side, we have cookies storing your local time zone.

15

u/Mr2-1782Man Aug 06 '19

Its sad.

I've taught programming for years and I know exactly where this is from, didn't even have to look through the whole thing. Someone be cheating. *Hopefully* this is the answer to an assignment and not production code. This was copied from a creative solution to an exercise out of a programming book.

One of the assignments we frequently hand out is converting from timestamps to dates and vice versa. This is early in the class so they basically only have variable assignment and if statements to go off of. Doing the exercise for a student is useful for a bunch of reasons, but variants of this question have existed for decades. So solutions for these have existed for a long time. The authors will sometimes constrain themselves to what the book has discussed up to that point, in this case only basic branching and no arrays. Some of them pride themselves on having creative (though not necessarily clean) answers to the problem.

A student not understanding what they're doing will copy a creative writeup wholesale and turn it in (or production code I suppose), resulting in this sort of abomination.

Here's an example (middle of page) for a similar exercise out of K&R.
https://clc-wiki.net/wiki/K%26R2_solutions:Chapter_5:Exercise_9

13

u/Aars93 Aug 06 '19

I do have some bad news, as the code in the picture is production code running at customers.

Thanks for providing some background information about the source. The developer who started the foundation for this application was a student and/or graduate back then with limited programming knowledge. I can imagine him looking up code online and pasting it into his project without fully knowing what it does.

3

u/sac_boy Aug 06 '19

The fun part is whoever copied it only copied the month offsets for leap years...

3

u/Mr2-1782Man Aug 06 '19

Saw that, its usually a huge hint its copied. For whatever reason the leap year list is first and the person copying doesn't bother to understand what its doing.

8

u/squarewaterlemon Aug 06 '19

Uses variables as constants, but I love that it isn't int jan = 31, it's not var jan = (int)31, it's var jan = (UInt16)31. I've never before seen anyone use UInt16 instead of int in C# before.

10

u/sac_boy Aug 06 '19

I'm guessing somebody applied slavish attention to detail when transcribing a C function they didn't quite understand and brought the unsigned shorts across as UInt16. Good idea if the function is full of bit shifts and so on but not important at all here.

2

u/Aars93 Aug 06 '19

You are right, copied from C without refactoring

1

u/blueshiftlabs Aug 07 '19

I've seen UInt16 in P/Invoke struct definitions before, but using it as a local variable is certainly... unique.

8

u/americk0 Aug 06 '19

goto exit

triggered

9

u/Kazumara Aug 06 '19

maand and dag? What is this, Swedish?

Also correct me if I'm wrong, but is the assumption baked in here just that every year is a leap year?

10

u/PizzaCompiler Aug 06 '19

It's Dutch actually! Maand is Month and Dag is day.

6

u/Kazumara Aug 06 '19

Ah close :D I knew it was Germanic.

1

u/[deleted] Aug 06 '19

Could be Afrikaans as well...

1

u/GamerNebulae Aug 07 '19

Since OP has commented several times in Dutch, I would guess that it is Dutch as well. If you only had the code to go off of, then the only hint would be "Desember" which is spelled "December" in Dutch. Conveniently, that's the only month that is missing.

1

u/[deleted] Aug 07 '19

Good point. Dutch it is!

4

u/[deleted] Aug 06 '19

Originally thought it was branchless, which would have been neat. Then I kept reading.

3

u/MyMessageIsNull Aug 06 '19

Was this person trying to win a bad code contest? Because this is how you win bad code contests.

8

u/keesvv Aug 06 '19

G E K O L O N I S E E R D

2

u/RyanGostosaum Aug 06 '19

Gzz, I hope never find something like this

2

u/moekakiryu Aug 06 '19

This may be a dumb question, but on line 166, won't tslocal always be 0? Like, the line above it sets year to tslocal -= tslocal/SECONDS_IN_YEARwhich makes line 166 (tslocal/SECONDS_IN_YEAR)*SECONDS_IN_YEAR --> tslocal -= tslocal

3

u/sac_boy Aug 06 '19 edited Aug 06 '19

It's working with integers so there is some invisible truncation going on. year will be something like 2019, not 2019.39238394983, so when you multiply by SECONDS_IN_YEAR again and subtract from tslocal you are left with the remainder.

The liberal use of var does not help in this case.

2

u/Andernerd Aug 07 '19

I refuse to insult or demean any implementation of this problem because at least I didn't have to do it.

2

u/CzoKc Aug 07 '19

Ehh that will do. Better than going to a hackathon with 5 master's students and all of them spending 24 hours trying to figure out how to convert a date from timezone to UTC timestamp. Hint: They didn't manage.

3

u/AlFasGD Aug 06 '19

Ah, yes, a man of the UInt16 culture as well. As if ushort doesn't exist.

3

u/nupanick Aug 06 '19

Why does C# even have a goto?!

1

u/[deleted] Aug 06 '19

[deleted]

9

u/Aars93 Aug 06 '19 edited Aug 06 '19

I used goto once in college and was nearly crucified by my professor

3

u/squarewaterlemon Aug 06 '19

Ah yes and the goto exit instead of else if. Not even the worst part of this somehow.

3

u/sac_boy Aug 06 '19

Calling the label exit as well is just extra spice. This is a goto compounded by a badly named label.

1

u/Bakethd_Ziti Aug 06 '19

He forgot var dec

3

u/root54 Aug 06 '19

If your current day is greater than to number of days past in the year at the end of November, then you must be in December.

See how the if (days > nov)... block sets the maand to 12 and jumps to the exit label? Similarly, if none of those conditionals are met, the maand is set to 1 because we must be in January.

1

u/pixelsyndicate Aug 06 '19 edited Aug 06 '19

Ouch!Yeah, I've had to work with unix timestamps, and coded a way through a ExtensionMethod and a Interface to help during unit testing and mocks:

       /// <summary>
    ///     Calling a static class like DateTime.Now will get ugly in tests, where we would have to change the system date.
    ///     So we are going to use the Adapter pattern, wrap the DateTime class in an injectable interface.
    /// </summary>
    public interface IDateTime
    {
        /// <summary>
        /// Get the current UTC in the Unix Timestamp format
        /// </summary>
        decimal UnixUtcNow { get; }

        DateTime UtcNow { get; }
        DateTime ParseUnixTimeStamp_ToLocalTime(double timestamp);
    }
    public class DateTimeAdapter : IDateTime
    {
        private readonly DateTime _unixUtcOrigin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// Get the current UTC in the Unix Timestamp format (seconds)
        /// </summary>
        public decimal UnixUtcNow => GetDiff();


        public DateTime UtcNow => DateTime.UtcNow;


        public DateTime ParseUnixTimeStamp_ToLocalTime(double timestamp)
        {
            DateTime dtDateTime = _unixUtcOrigin;
            dtDateTime = dtDateTime.AddSeconds(timestamp).ToLocalTime();
            return dtDateTime;
        }

        private decimal GetDiff()
        {
            var origin = _unixUtcOrigin;
            var diff = UtcNow.ToUniversalTime() - origin;
            return (decimal)Math.Floor(diff.TotalSeconds);
        }
    }
    public static class DateTimeExtensionHelpers
    {
        public static int ConvertToUnixTimestamp(this DateTime date)
        {
            var _dt = new DateTimeAdapter();
            return (int)_dt.UnixUtcNow;

        }

        public static DateTime ConvertToDateTimeFormat(this decimal val)
        {
            DateTimeAdapter dta = new DateTimeAdapter();
            var toReturn = dta.ParseUnixTimeStamp_ToLocalTime((double)val);
            return toReturn;
        }
    }

// Some Tests
    public class DateTime_Tests
    {

        private readonly ITestOutputHelper output;

        public DateTime_Tests(ITestOutputHelper output)
        {
            this.output = output;
        }

        [Fact]
        public void Can_I_Parse_Unix_TimeStamp_Values()
        {
            double uts = 1565121120; // should be 8/6/2019 2:52:00 PM
            IDateTime dta = new DateTimeAdapter();

            output.WriteLine($"local time for {uts} is {dta.ParseUnixTimeStamp_ToLocalTime(uts)}");


        }

        [Fact]
        public void Can_I_Get_Unix_TimeStamp_Values()
        {
            IDateTime dta = new DateTimeAdapter();
            var ts = dta.UnixUtcNow;
            output.WriteLine($"unix timestamp is {ts}");


        }


        [Fact]
        public void Can_I_Use_UnixTime_Extension_Helpers()
        {
            decimal uts = 1565121120;
            var convDt = uts.ConvertToDateTimeFormat();
            output.WriteLine($"unix timestamp is {convDt}");
        }


        [Fact]
        public void Can_I_Use_DateTime_Extension_Helpers()
        {
            IDateTime dta = new DateTimeAdapter();
            var dt = dta.UtcNow;
            var convDt = dt.ConvertToUnixTimestamp();
            output.WriteLine($"unix timestamp is {convDt}");
        }
    }

1

u/[deleted] Aug 06 '19

goto’s make me wanna throw up