2013-02-22 17 views
12

Sono nuovo di Delphi (ho programmato per circa 6 mesi). Finora, è stata un'esperienza estremamente frustrante, in gran parte derivante dal modo in cui Delphi si trova a gestire date e orari. Forse penso che sia male perché non so come usare correttamente TDate e TTime, non lo so. Ecco ciò che sta accadendo a me in questo momento:Come posso aggirare l'incapacità di Delphi di gestire accuratamente le manipolazioni datetime?

// This shows 570, as expected 
ShowMessage(IntToStr(MinutesBetween(StrToTime('8:00'), StrToTime('17:30')))); 

// Here I would expect 630, but instead 629 is displayed. WTF!? 
ShowMessage(IntToStr(MinutesBetween(StrToTime('7:00'), StrToTime('17:30')))); 

Questo non è il codice esatto che uso, tutto è nelle variabili e utilizzato in un altro contesto, ma penso che si può vedere il problema. Perché questo calcolo è sbagliato? Come suppongo di aggirare questo problema?

+0

Il grosso problema con 'TDateTime' di delphi è che sono doppi che rappresentano giorni, invece di punti fissi. Ciò significa che non possono rappresentare esattamente i minuti. (Non sono sicuro se questo è ciò che causa il problema, ma non sarei sorpreso) – CodesInChaos

+1

@CodesInChaos: Mi è stato detto. Non è una scusa adeguata per me. Potrebbero semplicemente farlo funzionare nel modo giusto invece di avere una [leaky astrazione] (http://www.joelonsoftware.com/articles/LeakyAbstractions.html). –

+0

Nelle versioni più recenti di delphi (2006 o giù di lì) è possibile creare i propri record che rappresentano date/ora e sovraccaricare gli operatori. Quindi è possibile eliminare i doppi dateti intrinsecamente difettosi. – CodesInChaos

risposta

19

Dato

a := StrToTime('7:00'); 
b := StrToTime('17:30'); 

ShowMessage(FloatToStr(a)); 
ShowMessage(FloatToStr(b)); 

il codice, utilizzando MinutesBetween, lo fa in modo efficace questo:

ShowMessage(IntToStr(trunc(MinuteSpan(a, b)))); // Gives 629 

Tuttavia, potrebbe essere meglio turno:

ShowMessage(IntToStr(round(MinuteSpan(a, b)))); // Gives 630 

Qual è in realtà il galleggiante -punto di valore?

ShowMessage(FloatToStr(MinuteSpan(a, b))); // Gives 630 

quindi si sta chiaramente soffrendo di problemi a virgola mobile tradizionali qui.

Aggiornamento:

Il vantaggio principale di Round è che se lo span minuto è molto vicino a un numero intero, il valore arrotondato sarà garantito essere che intero, mentre il valore troncato potrebbe benissimo essere precedente numero intero.

Il principale vantaggio di Trunc è che si potrebbe effettivamente desiderare questo tipo di logica: infatti, se si compie 18 anni in cinque giorni, legalmente non è ancora consentito richiedere una patente di guida svedese.

Quindi, se si desidera utilizzare Round invece di Trunc, si può semplicemente aggiungere

function MinutesBetween(const ANow, AThen: TDateTime): Int64; 
begin 
    Result := Round(MinuteSpan(ANow, AThen)); 
end; 

alla vostra unità. Quindi l'identificatore MinutesBetween farà riferimento a questo, nella stessa unità, anziché a quello in DateUtils. La regola generale è che il compilatore utilizzerà la funzione trovata più tardi. Così, per esempio, se si desidera mettere questa funzione in precedenza nella propria unità DateUtilsFix, quindi

implementation 

uses DateUtils, DateUtilsFix 

utilizzerà il nuovo MinutesBetween, dal momento che DateUtilsFix occurss alla destra di DateUtils.

Aggiornamento 2:

altro approccio potrebbe essere plausibile

function MinutesBetween(const ANow, AThen: TDateTime): Int64; 
var 
    spn: double; 
begin 
    spn := MinuteSpan(ANow, AThen); 
    if SameValue(spn, round(spn)) then 
    result := round(spn) 
    else 
    result := trunc(spn); 
end; 

Ciò restituirà round(spn) rappresenta l'intervallo è compreso nell'intervallo lanugine di un numero intero, e trunc(spn) altrimenti.

Ad esempio, utilizzando questo approccio

07:00:00 and 07:00:58 

produrrà 0 minuti, proprio come la versione basata su trunc originale, e proprio come il Trafikverket svedese vorrebbe. Ma non soffrirà il problema che ha innescato la domanda dell'OP.

+1

Questo approccio è a prova di proiettile –

+0

Sembra la strada da percorrere. Grazie! –

+0

Non sono sicuro di aver capito il tuo secondo aggiornamento. Che valore aggiunge? –

8

Questo è un problema risolto nelle ultime versioni di Delphi. Così si potrebbe effettuare l'aggiornamento, o semplicemente utilizzare il nuovo codice in Delphi 2010. Per esempio, questo programma produce l'output che ci si aspetta:

{$APPTYPE CONSOLE} 
uses 
    SysUtils, DateUtils; 

function DateTimeToMilliseconds(const ADateTime: TDateTime): Int64; 
var 
    LTimeStamp: TTimeStamp; 
begin 
    LTimeStamp := DateTimeToTimeStamp(ADateTime); 
    Result := LTimeStamp.Date; 
    Result := (Result * MSecsPerDay) + LTimeStamp.Time; 
end; 

function MinutesBetween(const ANow, AThen: TDateTime): Int64; 
begin 
    Result := Abs(DateTimeToMilliseconds(ANow) - DateTimeToMilliseconds(AThen)) 
    div (MSecsPerSec * SecsPerMin); 
end; 

begin 
    Writeln(IntToStr(MinutesBetween(StrToTime('7:00'), StrToTime('17:30')))); 
    Readln; 
end. 

Il codice Delphi 2010 per MinutesBetween assomiglia a questo:

function SpanOfNowAndThen(const ANow, AThen: TDateTime): TDateTime; 
begin 
    if ANow < AThen then 
    Result := AThen - ANow 
    else 
    Result := ANow - AThen; 
end; 

function MinuteSpan(const ANow, AThen: TDateTime): Double; 
begin 
    Result := MinsPerDay * SpanOfNowAndThen(ANow, AThen); 
end; 

function MinutesBetween(const ANow, AThen: TDateTime): Int64; 
begin 
    Result := Trunc(MinuteSpan(ANow, AThen)); 
end; 

Quindi, MinutesBetween si riduce effettivamente a una sottrazione a virgola mobile dei due valori di data/ora. A causa della intrinseca in-esattezza dell'aritmetica in virgola mobile, questa sottrazione può produrre un valore leggermente superiore o inferiore al valore vero. Quando è inferiore al valore reale, l'utilizzo di Trunc ti porterà fino al minuto precedente. La semplice sostituzione di Trunc con Round risolverebbe il problema.


Come accade nelle ultime versioni di Delphi, revisionare completamente i calcoli di data/ora. Ci sono importanti cambiamenti in DateUtils. È un po 'più difficile da analizzare, ma la nuova versione si basa su DateTimeToTimeStamp. Converte la porzione di tempo del valore nel numero di millisecondi dalla mezzanotte. E lo fa in questo modo:

function DateTimeToTimeStamp(DateTime: TDateTime): TTimeStamp; 
var 
    LTemp, LTemp2: Int64; 
begin 
    LTemp := Round(DateTime * FMSecsPerDay); 
    LTemp2 := (LTemp div IMSecsPerDay); 
    Result.Date := DateDelta + LTemp2; 
    Result.Time := Abs(LTemp) mod IMSecsPerDay; 
end; 

Nota l'uso di Round. L'uso di Round anziché di Trunc è il motivo per cui l'ultimo codice Delphi gestisce MinutesBetween in modo affidabile.


Supponendo che non è possibile aggiornare in questo momento, vorrei affrontare il problema in questo modo:

  1. Lascia il tuo codice invariato. Continua a chiamare MinutesBetween ecc.
  2. Quando esegui l'aggiornamento, il codice che chiama MinutesBetween ora funzionerà.
  3. Nel frattempo correggere MinutesBetween ecc. Con code hooks. Quando arrivi all'aggiornamento, puoi semplicemente rimuovere i ganci.