Delphi displays strange results for operations with negative values to TDateTime - datetime

We have a solution in Delphi that calculates a travel's duration of a given vehicle, for example, 20 minutes, 25 minutes and so on. However, sometimes we have to antecipate the travel's start time, from a specific datetime, for example 09:00 to 08:40. Then, we need to substract a negative value from a TDateTime variable (travel's start), in this case, something like "-00:20". To do this, we multiply the datetime value by -1 (for example MyDiffDateTimeVariable * -1). The output we got is very strange, sometimes we obtain the exactly opposite behavior. In other case, an operation to extract 20 minutes results in a difference of two days from the original datetime.
Here is a sample console application that simulate our situation, with the current outputs, and what we will expected:
program DateTimeSample;
uses
System.SysUtils, System.DateUtils;
var
LDate1: TDateTime;
LDate2: TDateTime;
begin
LDate1 := IncMinute(0, 20);
LDate2 := IncMinute(0, -20);
WriteLn('Date1: ' + DateTimeToStr(LDate1));
// Output = Date1: 30/12/1899 00:20:00 [OK]
WriteLn('Date2: ' + DateTimeToStr(LDate2));
// Output = Date2: 29/12/1899 23:40:00 [OK]
WriteLn('-----');
WriteLn('Date1: ' + DateTimeToStr(LDate1 * -1));
// Output = Date1: 30/12/1899 00:20:00 [Expected 29/12/1899 23:40:00]
WriteLn('Date2: ' + DateTimeToStr(LDate2 * -1));
// Output = Date2: 31/12/1899 23:40:00 [Expected 30/12/1899 00:20:00]
ReadLn;
end.

When you inspect the value casted to double, you can see:
double(LDate1) = 0.0138888888888889
double(LDate2) = -1.98611111111111
Seems like a bug to me, because with today it returns:
double(LDate1) = 43168,0138888889
double(LDate2) = 43167,9861111111
Edit: Hmm, according the documentation, it is not a bug, it is a feature :-)
When working with negative TDateTime values, computations must handle time portion separately. The fractional part reflects the fraction of a 24-hour day without regard to the sign of the TDateTime value. For example, 6:00 A.M. on December 29, 1899 is –1.25, not –1 + 0.25, which would equal –0.75. There are no TDateTime values from –1 through 0.

Karel's answer explains what's happening. Basically, TDateTime is represented as a Double, but that doesn't mean you can work with it in the same way as you normally would a Double value. It's internal structure carries particular semantics that if you don't handle them correctly, you're bound to get some peculiar behaviour.
The key mistake you're making is in taking the negative of a date-time value. This concept doesn't really make sense. Not even if you look at dates in BC, because the calendar system has changed a number of times over the years.
This is the main reason you should favour library routines that deal with the nuances of the internal structure (whatever your platform). In Delphi that means you should use the SysUtils and DateUtils routines for working with dates and times.
You seem to be trying to hold duration as a TDateTime value. You'd be much better off determining your preferred unit of measure and using Integer (perhaps Int64) or Double (if you need support for fractions of a unit). Then you can add or subtract, preferably using library routines, the duration from your start or end times.
The following code demonstrates some examples.
var
LStartTime, LEndTime: TDateTime;
LDuration_Mins: Integer;
begin
{ Init sample values for each calculation }
LStartTime := EncodeDateTime(2018, 3, 9, 8, 40, 0, 0);
LEndTime := EncodeDateTime(2018, 3, 9, 9, 0, 0, 0);
LDuration_Mins := 20;
{ Output result of each calculation }
Writeln(Format('Whole Duration: %d', [MinutesBetween(LStartTime, LEndTime)]));
Writeln(Format('Frac Duration: %.6f', [MinuteSpan(LStartTime, LEndTime)]));
Writeln(Format('Start Time: %s', [FormatDateTime('yyyy-mm-dd hh:nn:ss', IncMinute(LEndTime, -LDuration_Mins))]));
Writeln(Format('End Time: %s', [FormatDateTime('yyyy-mm-dd hh:nn:ss', IncMinute(LStartTime, LDuration_Mins))]));
end;
Additional Considerations
You said you're dealing with vehicle travel times. If you're dealing with long-haul travel you might have some other things to think about.
Daylight saving: If a vehicle starts its journey shortly before DST changes and ends after, you need to take this into account when calculating a missing value. Perhaps easiest would be to convert date-time values to UTC for the calculation. Which leads to...
Time zone changes: Again, unless your code is time-zone aware you're bound to make mistakes.

Compiler always appears to treat TDateTime as positive when doing numerical operations on it. Try this:
uses
System.SysUtils, System.DateUtils;
function InvertDate(ADateTime: TDateTime): TDateTime;
var
LMsec: Int64;
begin
LMsec := MillisecondsBetween(ADateTime, 0); //Always Positive
if ADateTime > 0 then
LMsec := 0 - LMsec;
Result := IncMillisecond(0, LMsec);
end;
var
LDate1: TDateTime;
LDate1Negative: TDateTime;
LDate2: TDateTime;
begin
try
LDate1 := IncMinute(0, 20);
LDate2 := IncMinute(0, -20);
WriteLn('Date1: ' + DateTimeToStr(LDate1));
// Output = Date1: 30/12/1899 00:20:00 [OK]
WriteLn('Date2: ' + DateTimeToStr(LDate2));
// Output = Date2: 29/12/1899 23:40:00 [OK]
WriteLn('-----');
WriteLn('Date1: ' + DateTimeToStr( InvertDate(LDate1) ));
// Output = Date1: Expected 29/12/1899 23:40:00
WriteLn('Date2: ' + DateTimeToStr( InvertDate(LDate2) ));
// Output = Date2: 30/12/1899 00:20:00
ReadLn;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.

Related

What does NNN mean in date format <YYMMDDhhmmssNNN><C|D|G|H>?

hi I has date format and I want converted to correct GMT date :
<YYMMDDhhmmssNNN><C|D|G|H>
Sample value on that date:
210204215026000C
I get this explanation for part NNN :
NNN If flag is C or D then NNN is the number of hours relativeto GMT,
if flag is G or H, NNN is the number of quarter hours relative to GMT
C|D|G|H C and G = Ahead of GMT, D and H = Behind GMT
but I did not get how number of hours relative to GMT can present on 3 digits ? it should be in 2 digit as i knew the offset for hours related to GMT is from 0 to 23 , and also what quarter hours relative to GMT mean ?
I want to use Scala or Java.
I don’t know why they set 3 digits aside for the offset. I agree with you that 2 digits suffice for all cases. Maybe they just wanted to be very sure they would never run of out space, and maybe they even overdid this a bit. 3 digits is not a problem as long as the actual values are within the range that java.time.ZoneOffset can handle, +/-18 hours. In your example NNN is 000, so 0 hours from GMT, which certainly is OK and trivial to handle.
A quarter hour is a quarter of an hour. As Salman A mentioned in a comment, 22 quarter hours ahead of Greenwich means an offset of +05:30, currently used in Sri Lanka and India. If the producer of the string wants to use this option, they can give numbers up to 72 (still comfortably within 2 digits). 18 * 4 = 72, so 18 hours equals 72 quarter hours. To imagine a situation where 2 digits would be too little, think an offset of 25 hours. I wouldn’t think it realistic, on the other hand no one can guarantee that it will never happen.
Java solution: how to parse and convert to GMT time
I am using these constants:
private static final Pattern DATE_PATTERN
= Pattern.compile("(\\d{12})(\\d{3})(\\w)");
private static final DateTimeFormatter FORMATTER
= DateTimeFormatter.ofPattern("uuMMddHHmmss");
private static final int SECONDS_IN_A_QUARTER_HOUR
= Math.toIntExact(Duration.ofHours(1).dividedBy(4).getSeconds());
Parse and convert like this:
String sampleValue = "210204215026000C";
Matcher matcher = DATE_PATTERN.matcher(sampleValue);
if (matcher.matches()) {
LocalDateTime ldt = LocalDateTime.parse(matcher.group(1), FORMATTER);
int offsetAmount = Integer.parseInt(matcher.group(2));
char flag = matcher.group(3).charAt(0);
// offset amount denotes either hours or quarter hours
boolean quarterHours = flag == 'G' || flag == 'H';
boolean negative = flag == 'D' || flag == 'H';
if (negative) {
offsetAmount = -offsetAmount;
}
ZoneOffset offset = quarterHours
? ZoneOffset.ofTotalSeconds(offsetAmount * SECONDS_IN_A_QUARTER_HOUR)
: ZoneOffset.ofHours(offsetAmount);
OffsetDateTime dateTime = ldt.atOffset(offset);
OffsetDateTime gmtDateTime = dateTime.withOffsetSameInstant(ZoneOffset.UTC);
System.out.println("GMT time: " + gmtDateTime);
}
else {
System.out.println("Invalid value: " + sampleValue);
}
Output is:
GMT time: 2021-02-04T21:50:26Z
I think my code covers all valid cases. You will probably want to validate that the flag is indeed C, D, G or H, and also handle the potential DateTimeException and NumberFormatException from the parsing and creating the ZoneOffset (NumberFormatException should not happen).

momentjs calculates date difference incorrectly

In my angular web application, I want to compare two dates to see if a person is less than 18 years old when she/he entered the company. Here is the code I use to do this:
const dayOfBirth = moment(formControl.value, this.dateFormat, true).startOf('day');
const entranceDateControl = this.wizardFormGroup.get('entranceDate');
const entranceDate = moment(entranceDateControl.value, this.dateFormat, true).startOf('day');
// Check validation rule R3: Age is less than 18 compared to entrance date
const difference = moment.duration(Math.abs(entranceDate.diff(dayOfBirth)));
if (difference.years() < 18) {
const validationMessage = this.getValidationMessage('R3', formControlName);
return validationMessage ? validationMessage.message : null;
}
As you can see, I am using startOf('day') to get rid of any time component so that I only handle dates. I use diff() to get the difference between two dates and then duration() to convert the difference to years, months, days, etc. Using this code, the validation message should NOT show when the person is turning 18 years old on the day when she/he entered the company.
Upon testing this, I came across what is, in my opinion, strange behavior. Depending on months and years used, it gave different results. For instance, for these dates it was Ok:
dayOfBirth = 1998-03-01, 1998-04-01, ..., 2000-02-01
entranceDate = 2016-03-01, 2016-04-01, ..., 2018-02-01
But the following dates returned the validation message:
dayOfBirth = 2000-03-01, 2000-04-01, ..., 2002-02-01
entranceDate = 2018-03-01, 2000-04-01, ..., 2020-02-01
After these dates, i.e. using 2002-03-01 and onward, it works again. I also got wrong result for the dates preceding 1998-03-01.
Now, I had a closer look at the Duration object and I noticed that for the times where it was less than 18 years, it had calculated 864 milliseconds less then when it came to the right conclusion that it was 18 years between the dates.
Correct duration
----------------
dayOfBirth = 1998-03-01, 1998-04-01, ..., 2000-02-01
entranceDate = 2016-03-01, 2016-04-01, ..., 2018-02-01
Duration = 568080000000 ms
Wrong duration
--------------
dayOfBirth = 2000-03-01, 2000-04-01, ..., 2002-02-01
entranceDate = 2018-03-01, 2000-04-01, ..., 2020-02-01
Duration = 567993600000 ms
Duration difference
-------------------
568080000000 - 567993600000 = 86400000 ms = 24 hours = 1 day
Has anyone an explanation for this? Can it be considered a bug in momentjs? Any viable workaround for this?
I didn't go into details in moment source code but it seems duration() is playing tricks with you. Simplify the code and rely only on diffas follow and you should be good (at least it seems to work for the samples you provided). And it's easier on the eyes :)
const moment = require('moment')
const dayOfBirth = moment('2000-03-01').startOf('day');
const entranceDate = moment('2018-03-01').startOf('day');
const difference = entranceDate.diff(dayOfBirth, 'years')
if (difference < 18) {
console.log( '<18')
} else {
console.log( '>=18')
}
will output >=18

Erlang: timestamp with time zone arithmetics

What is the best way to add/subtract units to/from specific timestamp with respect to time zone in Erlang?
From what I've found, calendar of stdlib can work with either local or UTC time zone, no more. Moreover, arithmetics is recommended to do in UTC time zone only (the reason is obvious).
What should I do if, for instance, I need to add 1 month to {{2011,3,24},{11,13,15}} in, let's say, CET (Central European Time) and local (system) time zone is not CET? That is not even the same as converting this timestamp to UTC, adding 31 * 24 * 60 * 60 seconds and converting back to CET (that will give {{2011,4,24},{12,13,15}} instead of {{2011,4,24},{11,13,15}}). By the way we can't do even such a thing if CET is not local time zone with stdlib.
The answers I found googling are:
setenv to make local time zone = needed time zone (that is very ugly first of all; then it will only allow to convert needed time zone to utc and do arithmetics respective to utc, not the needed time zone)
open_port to linux date util and do arithmetics there (not that ugly; rather slow; needs some parsing, because the protocol between erlang and date will be textual)
port driver or erl_interface to C using its standard library (not ugly at all; but I didn't find ready to use solution and I'm not that good at C to write one)
The ideal solution would be something written in Erlang using OS timezone info, but I didn't find any.
Now I'm stuck to solution 2 (open_port to date util). Is there a better way?
Thanks in advance.
P. S. There was a similar issue, but no good answer there Time zone list issue
port_helper.erl
-module(port_helper).
-export([get_stdout/1]).
get_stdout(Port) ->
loop(Port, []).
loop(Port, DataAcc) ->
receive
{Port, {data, Data}} ->
loop(Port, DataAcc ++ Data);
{Port, eof} ->
DataAcc
end.
timestamp_with_time_zone.erl
-module(timestamp_with_time_zone).
-export([to_time_zone/2, to_universal_time/1, modify/2]).
to_time_zone({{{Year, Month, Day}, {Hour, Minute, Second}}, TimeZone}, OutputTimeZone) ->
InputPattern = "~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B",
InputDeep = io_lib:format(InputPattern, [Year, Month, Day, Hour, Minute, Second]),
Input = lists:flatten(InputDeep),
{external_date(Input, TimeZone, OutputTimeZone), OutputTimeZone}.
to_universal_time({{{Year, Month, Day}, {Hour, Minute, Second}}, TimeZone}) ->
{Timestamp, "UTC"} = to_time_zone({{{Year, Month, Day}, {Hour, Minute, Second}}, TimeZone}, "UTC"),
Timestamp.
modify({{{Year, Month, Day}, {Hour, Minute, Second}}, TimeZone}, {Times, Unit}) ->
if
Times > 0 ->
TimesModifier = "";
Times < 0 ->
TimesModifier = " ago"
end,
InputPattern = "~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B ~.10B ~s~s",
InputDeep = io_lib:format(InputPattern, [Year, Month, Day, Hour, Minute, Second, abs(Times), Unit, TimesModifier]),
Input = lists:flatten(InputDeep),
external_date(Input, TimeZone, TimeZone).
external_date(Input, InputTimeZone, OutputTimeZone) ->
CmdPattern = "date --date 'TZ=\"~s\" ~s' +%Y%m%d%H%M%S",
CmdDeep = io_lib:format(CmdPattern, [InputTimeZone, Input]),
Cmd = lists:flatten(CmdDeep),
Port = open_port({spawn, Cmd}, [{env, [{"TZ", OutputTimeZone}]}, eof, stderr_to_stdout]),
ResultString = port_helper:get_stdout(Port),
case io_lib:fread("~4d~2d~2d~2d~2d~2d", ResultString) of
{ok, [YearNew, MonthNew, DayNew, HourNew, MinuteNew, SecondNew], _LeftOverChars} ->
{{YearNew, MonthNew, DayNew}, {HourNew, MinuteNew, SecondNew}}
end.

displaying current minute of a soccer match

I could not figure out the logic I would need to display a current minute of a soccer match. I have three fields in the database.
DateFirstStarted
DateSecondStarted
DateFullEnded
I should enter DateFirstStarted when the game starts and on the website. i.e. game starts at 7:05pm, on 7:25pm it should display '20 on the website. However, it should stop on the 45th minute. Then, I enter DateSecondStarted when the second half starts and should count from 46 to 90 and freeze there. Do I make sense? How can I do it? Is there a better way to do this?
Something not too complicated should do it. I will update start date of first half and start date of second half myself.
Here is how I tried it. I dont get an error but its not working. Any suggestion appreciated.
DateFirstStarted = objLiveCommentary("DateFirstStarted")
DateFirstEnded = DateAdd("n", 45, DateFirstStarted)
DateSecondStarted = objLiveCommentary("DateSecondStarted")
DateSecondEnded = DateAdd("n", 45, DateSecondStarted)
If DateFirstStarted => NOW() => DateFirstEnded Then
Response.Write "first half"
ElseIf DateSecondStarted => NOW() => DateSecondEnded Then
Response.Write "second half"
End If
Have a look at the DateDiff function?
You pass it two datetimes and it will give you the total amount of mins / seconds / hours etc between the two values.
http://www.w3schools.com/vbScript/func_datediff.asp
E.g.
If DateDiff("n",StartOfMatch,Now()) < 45 Then
FirstHalf = True
Elseif DateDiff("n",StartOfMatch,Now()) >= 45 Then
SecondHalf = True
Else
MatchEnded = True
End If
Hope this helps you?
C

Weird flex date issue

Flex is driving me CRAZY and I think it's some weird gotcha with how it handles leap years and none leap years. So here's my example. I have the below dateDiff method that finds the number of days or milliseconds between two dates. If I run the following three statements I get some weird issues.
dateDiff("date", new Date(2010, 0,1), new Date(2010, 0, 31));
dateDiff("date", new Date(2010, 1,1), new Date(2010, 1, 28));
dateDiff("date", new Date(2010, 2,1), new Date(2010, 2, 31));
dateDiff("date", new Date(2010, 3,1), new Date(2010, 3, 30));
If you were to look at the date comparisons above you would expect to get 30, 27, 30, 29 as the number of days between the dates. There weird part is that I get 29 when comparing March 1 to March 31. Why is that? Is it something to do with February only having 28 days? If anyone has ANY input on this that would be greatly appreciated.
public static function dateDiff( datePart:String, startDate:Date, endDate:Date ):Number
{
var _returnValue:Number = 0;
switch (datePart) {
case "milliseconds":
_returnValue = endDate.time - startDate.time;
break;
case "date":
// TODO: Need to figure out DST problem i.e. 23 hours at DST start, 25 at end.
// Math.floor causes rounding down error with DST start at dayOfYear
_returnValue = Math.floor(dateDiff("milliseconds", startDate, endDate)/(1000 * 60 * 60 * 24));
break;
}
return _returnValue;
}
This is not a leap year problem, but rather a daylight savings time problem.
To correct the code to account for DST, you need to look at the timezoneOffset of both dates to determine if the date range is spanning a DST boundary.
var adjustment:Number = ( startDate.timezoneOffset - endDate.timezoneOffset ) * 60 * 1000;
_returnValue = endDate.time - startDate.time + adjustment;
This will get the difference between the two time zones (in minutes), convert that value to milliseconds, and then apply the timezone difference to the millisecond difference to "cancel out" the DST boundary.
Naturally, when both numbers are in the same time zone, the adjustment value becomes 0 and the time values are not adjusted.
You have part of the answer in your comment: 2010-Mar-01 0:00 until 2010-Mar-31 0:00 is thirty (!) days minus one hour (because Mar 14 is DST start in 2010). Since you floor the result of your division, you get 29.
Edit: This answer is of course based on the assumption that the time property of Date takes DST into account. This would explain your problem; I didn't check it, however.

Resources