Ogarnąłem parę razy artykuł o Obliczeniach na datach i czasie i nie mogę znaleźć takiej która zwróciłaby ilość lat, miesięcy, dni, godzin, minut i sekund pomiędzy dwoma datami. Więc czy faktycznie, w tak bogatym zbiorze delphi, jest taka funkcja czy muszę ją samemu napisać?
jest, nazywa się odejmowanie.
facepalm...
data1-data2
Nie ma. Musisz sam napisać. Najpierw rozkładasz wartość datę na rok/mies/dzień itd. (od tego są funkcje - > DateUtils) a później odejmujesz kolejne składniki. Tylko i tak jest to problematyczne, gdyż nie możesz miesiąca na dni przeliczyć. Będzie ci potrzebne sprawdzanie ile dni ma dany miesiąc.
Nie ma. Musisz sam napisać.
No jasne, że nie ma:
Jak nie znasz się, to chociaż czytaj to co inni napisali :X
Nie ma takiej funkcji o jaką autor pyta. Jeśli jest, to wskaż ją.
DaySpan
,HourSpan
itd. Czy na pewno nie ma? Są.
Te funkcje na niewiele się zdadzą. Widać nie rozumiesz sedna problemu.
DecodeDateTime
W module DataUtils
Znalazłem funcję IncDays
, IncHours
etc, przerobiłem je trochę. Do tego napisałem funkcję która zwraca poprawną odmianę słów dzień, godzina, minuta, sekunda
.
type
TMyLabel = class(TLabel)
public
procedure Add(const S: String);
procedure Clear;
end;
TFormatType = (fDays, fHours, fMinutes, fSeconds);
var
MainForm: TMainForm;
Labela: TMyLabel;
function GetDateString(I: Integer; Format: TFormatType): String;
var Licznik, Last: Integer; S: String;
const Tab: array [TFormatType, 1..3] of String =
(('dzień', 'dni', 'dni'),
('godzina', 'godziny', 'godzin'),
('minuta', 'minuty', 'minut'),
('sekunda', 'sekundy', 'sekund'));
begin
S := IntToStr(I);
Last := StrToInt(S[Length(S)]);
Licznik := 3;
if (Last >= 2) and (Last <= 4) then Licznik := 2;
if I = 1 then Licznik := 1;
Result := Tab[Format, Licznik];
end;
procedure TMyLabel.Add(const S: String);
begin
Caption := Caption + S;
end;
procedure TMyLabel.Clear;
begin
Caption := '';
end;
function DecDay(const AValue: TDateTime;
const ANumberOfDays: Integer): TDateTime;
begin
Result := AValue - ANumberOfDays;
end;
function DecHour(const AValue: TDateTime;
const ANumberOfHours: Int64): TDateTime;
begin
Result := ((AValue * HoursPerDay) - ANumberOfHours) / HoursPerDay;
end;
function DecMinute(const AValue: TDateTime;
const ANumberOfMinutes: Int64): TDateTime;
begin
Result := ((AValue * MinsPerDay) - ANumberOfMinutes) / MinsPerDay;
end;
function DecSecond(const AValue: TDateTime;
const ANumberOfSeconds: Int64): TDateTime;
begin
Result := ((AValue * SecsPerDay) - ANumberOfSeconds) / SecsPerDay;
end;
procedure DoAllThis;
var Data: TDateTime; Licznik: Integer; {tą procedurę regularnie wywołuje Timer}
begin
Data := EncodeDateTime(2012, 07, 01, 0, 0, 0, 0);
Labela.Clear;
Labela.Add('Do wakacji zostało ');
Licznik := DaysBetween(Data, Now);
Labela.Add(IntToStr(Licznik) + ' ' + GetDateString(Licznik, fDays) + ', ');
Data := DecDay(Data, Licznik);
Licznik := HoursBetween(Data, Now);
Labela.Add(IntToStr(Licznik) + ' ' + GetDateString(Licznik, fHours) + ', ');
Data := DecHour(Data, Licznik);
Licznik := MinutesBetween(Data, Now);
Labela.Add(IntToStr(Licznik) + ' ' + GetDateString(Licznik, fMinutes) + ' i ');
Data := DecMinute(Data, Licznik);
Licznik := SecondsBetween(Data, Now);
Labela.Add(IntToStr(Licznik) + ' ' + GetDateString(Licznik, fSeconds));
end;
Umieszczam tutaj, bo może komuś się przyda.
Mam teraz więcej czasu więc wytłumaczę wszystko i dam przykładowy kod.
Zadanie wydaje się być proste ale tak naprawdę diabeł tkwi w środku. Spowodowane jest to pokręconą arytmetyką narzuconą sposobem w jakim reprezentujemy datę. Po pierwsze trzeba się liczyć z tym iż dostaniemy wyniki dziwne. Dla przykładu:
28.02.2011 - 31.03.2011 da w wyniku 1 miesiąc i 3 dni, gdyż pierwszy miesiąc mija 28.03.2011 a do 31.03.2011 zostają jeszcze 3 dni
28.02.2011 - 1.05.2011 da w wyniku 1 miesiąc i 1 dzień, gdyż do końca lutego jest 1 dzień i później mamy cały pełny miesiąc. Czyli mniej niż poprzednio!
Tradycyjna arytmetyka niestety zawodzi. Nie możemy sobie po kolei wywołać (Days/Monts/Yers)Between i zwyczajnie skumulować wynik. Trzeba trochę pokombinować. Tak więc przejdźmy może do implementacji (uwaga, dawno nie pisałem w tym języku, poniższego kodu nie kompilowałem):
procedure SwapDateTime(var A, B: TDateTime);
var
tmp: TDateTime;
begin
tmp := B;
B := A;
A := tmp;
end;
procedure GetDateTimeSpan(DateTimeFrom, DateTimeTo: TDateTime; var Years, Months, Days, Hours, Minutes, Seconds: Word);
var
TimeDiff : TDateTime;
MonthDiff: Integer;
MSec: Word;
const
MONTHS_IN_YEAR = 12;
MAX_DAYS_DIFF = 30; // największy możliwy wynik ilości dni
begin
{ w zasadzie tylko jeden kierunek ma sens więc ustawmy datę mniejszą jako pierwszą }
if DateTimeFrom > DateTimeTo then SwapDateTime(DateTimeFrom, DateTimeTo);
{ sporą robotę odwala funkcja MonthsBetween, obliczamy lata i miesiące ZANIM zaczniemy
modyfikować daty na potrzeby algorytmu gdyż ona zwróci od razu prawidłowy wynik }
MonthDiff := MonthsBetween(DateTimeFrom, DateTimeTo);
Years := MonthDiff div MONTHS_IN_YEAR; // na szczęście rok to zawsze 12 miesięcy
Months := MonthDiff mod MONTHS_IN_YEAR;
{ Aby dotrzeć do ilości dni musimy przetworzyć czas dnia, obliczmy więc ich różnicę. Jeśli czas dnia
końcowego nie osiągnął jeszcze czasu dnia początkowego, wtedy liczmy czas do końca dnia
początkowego + czas od początku dnia końcowego. Obydwa przypadki obejmie następująca konstrukcja. }
TimeDiff := TimeOf(DateTimeTo - DateTimeFrom);
{ obliczony czas zamieniamy na składniki }
DecodeTime(TimeDiff, Hours, Minutes, Seconds, MSec);
{ TU UWAGA: licząc dni możemy powiedzieć, że jeśli czas dnia "DateTimeTo" jeszcze nie osiągnął
czasu dnia "DateTimeFrom" to data końcowa znajduje się dzień wcześniej, gdyż odpowiednia
godzina dnia nie została jeszcze osiągnięta. Dlatego odejmujemy jeden dzień od daty końcowej
i nie bierzemy już czasu dnia pod uwagę }
if TimeOf(DateTimeTo) < TimeOf(DateTimeFrom) then DateTimeTo := DecDay(DateTimeTo);
{ Teraz kolejne bardzo pomocne funkcje - DayOfTheMonth i DaysInMonth,
UWAGA: przed ich użyciem musieliśmy wziąć pod uwagę czas dnia, teraz go możemy
zignorować. Inaczej na nie wiele by się te funkcje zdały.
Obliczamy różnicę dnia miesiąca. }
Days := DayOfTheMonth(DateTimeTo) - DayOfTheMonth(DateTimeFrom);
{ W przypadku gdy dni następują po sobie nie ma problemu, dostaniemy wynik z przedziału 0..30;
w przeciwnym razie wynik będzie "ujemny" t.j. wartość będzie po prostu poza zakresem.
Będzie tak, gdy dzień miesiąca daty początkowej nie osiągnie jeszcze dnia miesiąca daty
końcowej. Wtedy, podobnie jak przy czasie dnia, obliczamy ilość dni do końca miesiąca
początkowego + ilość dni od początku miesiąca końcowego. Zrobimy to za jednym zamachem
dodając do wyniku ilość dni miesiąca początkowego. Zauważ, że nie musimy tego
dodanego miesiąca uwzględniać już w wyniku ilości miesięcy, funkcja MonthsBetween na prawdę
sporo odwaliła. }
if not Days in [0..MAX_DAYS_DIFF] then Inc(Days, DaysInMonth(DateTimeFrom));
end;
Procedura pochodzi ze strony: http://www.delphipages.com/forum/showthread.php?t=141632
procedure DateDif(Date1, Date2: TDateTime; var Y, M, D: word);
var
d1, d2 : TDateTime;
begin
Y := 0;
M := 0;
D := 0;
if Trunc(Date1) < Trunc(Date2) then begin
d1 := Trunc(Date1);
d2 := Trunc(Date2);
end else begin
d1 := Trunc(Date2);
d2 := Trunc(Date1);
end;
while Trunc(IncMonth(d1)) <= Trunc(d2) do begin
d1 := IncMonth(d1);
Inc(M);
end;
while Trunc(d1) +1 <= Trunc(d2) do begin
d1 := d1 +1;
Inc(D);
end;
Y := M div 12;
M := M -(Y *12);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Y, M, D : word;
begin
DateDif(DateTimePicker1.DateTime, DateTimePicker2.DateTime, Y, M, D);
YearsEdit.Text := IntToStr(Y);
MonthsEdit.Text := IntToStr(M);
DaysEdit.Text := IntToStr(D);
end;