Różnica między datą i godziną w Ruby

219

Jaka jest różnica między DateTimei Timeklas w Ruby i jakie czynniki spowodowałoby mi wybrać jedną lub drugą stronę?

Tom Lehman
źródło
Dokumenty zawierają sekcję wyjaśniającą, kiedy tego użyć.
x-yuri

Odpowiedzi:

177

Nowsze wersje Ruby (2.0+) tak naprawdę nie mają znaczących różnic między dwiema klasami. Niektóre biblioteki będą używać jednej lub drugiej ze względów historycznych, ale nowy kod niekoniecznie musi dotyczyć. Wybieranie jednego w celu uzyskania spójności jest prawdopodobnie najlepsze, więc spróbuj połączyć się z oczekiwaniami bibliotek. Na przykład ActiveRecord woli DateTime.

W wersjach wcześniejszych niż Ruby 1.9 i na wielu systemach czas jest reprezentowany jako 32-bitowa wartość ze znakiem opisująca liczbę sekund od 1 stycznia 1970 roku UTC, cienkie opakowanie wokół time_twartości standardu POSIX , i jest ograniczona:

Time.at(0x7FFFFFFF)
# => Mon Jan 18 22:14:07 -0500 2038
Time.at(-0x7FFFFFFF)
# => Fri Dec 13 15:45:53 -0500 1901

Nowsze wersje Ruby są w stanie obsługiwać większe wartości bez powodowania błędów.

DateTime to podejście oparte na kalendarzu, w którym rok, miesiąc, dzień, godzina, minuta i sekunda są przechowywane osobno. Jest to konstrukcja Ruby on Rails, która służy jako opakowanie wokół standardowych pól SQL DATETIME. Zawierają one dowolne daty i mogą reprezentować prawie dowolny punkt w czasie, ponieważ zakres wyrażania jest zazwyczaj bardzo duży.

DateTime.new
# => Mon, 01 Jan -4712 00:00:00 +0000

Dlatego uspokajające jest to, że DateTime może obsłużyć posty na blogu od Arystotelesa.

Wybierając jedną, różnice są teraz nieco subiektywne. Historycznie DateTime zapewnia lepsze opcje manipulowania nim w sposób kalendarzowy, ale wiele z tych metod zostało przeniesionych również do Time, przynajmniej w środowisku Rails.

tadman
źródło
6
Czy powinienem zawsze używać DateTime?
Tom Lehman,
4
Jeśli pracujesz z datami, powiedziałbym, że używaj DateTime. Czas jest dogodny do reprezentowania rzeczy, takich jak bieżąca pora dnia, lub punktów w najbliższej przyszłości, takich jak 10. minut. Od. Obie mają ze sobą wiele wspólnego, choć jak zauważono, DateTime może reprezentować znacznie szerszy zakres wartości.
tadman
3
Uważam, że dzieje się tak, ponieważ Ruby przechodzi na Bignum o dowolnej długości z 32-bitowego Fixnum, gdy występuje przepełnienie. Numery spoza tego zakresu mogą nie być obsługiwane przez aplikacje zewnętrzne. Tak, w 2038 roku jesteśmy w zasadzie popieprzeni, dopóki wszyscy nie uzgodnimy właściwego 64-bitowego formatu czasu. Jury jest nadal nieobecne.
tadman
28
Ta odpowiedź jest wcześniejsza niż 1.9.2. Zignoruj ​​wszystko, co mówi o ograniczeniach POSIX czasu, i dokonaj wyboru na podstawie interfejsów API czasu i daty.
Ben Nagy
8
Ta odpowiedź jest nieaktualna, prawda? Teraz zaleca się użycie funkcji TimeSupport :: Time lub Rail :: WithTimeZone. Nie musisz już używać DateTime. Jest tam dla kompatybilności wstecznej.
Donato,
103

[Edytuj lipiec 2018 r.]

Wszystkie poniższe elementy są nadal aktualne w Ruby 2.5.1. Z dokumentacji referencyjnej :

DateTime nie uwzględnia sekund przestępnych, nie śledzi żadnych reguł dotyczących czasu letniego.

To, czego wcześniej nie zauważono w tym wątku, jest jedną z niewielu zalet DateTime: jest świadomy reform kalendarza, podczas gdy Timenie jest:

[…] Klasa Ruby Time realizuje proleptyczny kalendarz gregoriański i nie ma pojęcia reformy kalendarza […].

Dokumentacja referencyjna kończy się zaleceniem, które należy stosować Timewyłącznie w przypadku bliskich, bieżących lub przyszłych dat / godzin i stosować tylko DateTimewtedy, gdy na przykład urodziny Szekspira wymagają dokładnej konwersji: (podkreślenie dodane)

Więc kiedy powinieneś używać DateTime w Ruby i kiedy powinieneś używać Time? Niemal na pewno będziesz chciał użyć Time, ponieważ Twoja aplikacja prawdopodobnie zajmuje się bieżącymi datami i godzinami. Jeśli jednak musisz poradzić sobie z datami i godzinami w kontekście historycznym, powinieneś użyć DateTime […]. Jeśli masz również do czynienia ze strefami czasowymi, powodzenia - pamiętaj, że prawdopodobnie będziesz mieć do czynienia z lokalnymi czasami słonecznymi, ponieważ dopiero w XIX wieku wprowadzenie kolei wymagało czasu standardowego i ostatecznie strefy czasowe.

[/ Edytuj lipiec 2018 r.]

Począwszy od Ruby 2.0, większość informacji zawartych w innych odpowiedziach jest nieaktualna.

W szczególności Timejest teraz praktycznie niezwiązany. Może być mniej więcej niż 63 bity od Epoki:

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
irb(main):002:0> Time.at(2**62-1).utc # within Integer range
=> 146138514283-06-19 07:44:38 UTC
irb(main):003:0> Time.at(2**128).utc # outside of Integer range
=> 10783118943836478994022445751222-08-06 08:03:51 UTC
irb(main):004:0> Time.at(-2**128).utc # outside of Integer range
=> -10783118943836478994022445747283-05-28 15:55:44 UTC

Jedyną konsekwencją użycia większych wartości powinna być wydajność, która jest lepsza, gdy Integerstosuje się s (vs. Bignums (wartości poza Integerzakresem) lub Rationals (gdy śledzone są nanosekundy)):

Od wersji Ruby 1.9.2 implementacja Time używa podpisanej 63-bitowej liczby całkowitej, Bignum lub Rational. Liczba całkowita jest liczbą nanosekund od epoki, która może reprezentować 1823-11-12 do 2116-02-20. Gdy używany jest Bignum lub Rational (przed 1823 r., Po 2116 r., W nanosekundach), czas działa wolniej, jak w przypadku użycia liczby całkowitej. ( http://www.ruby-doc.org/core-2.1.0/Time.html )

Innymi słowy, o ile rozumiem, DateTimenie obejmuje już szerszego zakresu potencjalnych wartości niżTime .

Ponadto DateTimeprawdopodobnie należy zauważyć dwa wcześniej niewymienione ograniczenia :

DateTime nie uwzględnia żadnych przerw, nie śledzi żadnych reguł dotyczących czasu letniego. ( http://www.ruby-doc.org/stdlib-2.1.0/libdoc/date/rdoc/Date.html#class-Date-label-DateTime )

Po pierwsze, DateTimenie ma pojęcia sekund przestępnych:

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
irb(main):002:0> require "date"
=> true
irb(main):003:0> t = Time.new(2012,6,30,23,59,60,0)
=> 2012-06-30 23:59:60 +0000
irb(main):004:0> dt = t.to_datetime; dt.to_s
=> "2012-06-30T23:59:59+00:00"
irb(main):005:0> t == dt.to_time
=> false
irb(main):006:0> t.to_i
=> 1341100824
irb(main):007:0> dt.to_time.to_i
=> 1341100823

Aby powyższy przykład działał Time, system operacyjny musi obsługiwać sekundy przestępne, a informacje o strefie czasowej muszą być ustawione poprawnie, np. Poprzez TZ=right/UTC irb(w wielu systemach uniksowych).

Po drugie, DateTimema bardzo ograniczone rozumienie stref czasowych, a w szczególności nie ma koncepcji oszczędności w świetle dziennym . Prawie obsługuje strefy czasowe jako proste przesunięcia UTC + X:

irb(main):001:0> RUBY_VERSION
=> "2.0.0"
irb(main):002:0> require "date"
=> true
irb(main):003:0> t = Time.local(2012,7,1)
=> 2012-07-01 00:00:00 +0200
irb(main):004:0> t.zone
=> "CEST"
irb(main):005:0> t.dst?
=> true
irb(main):006:0> dt = t.to_datetime; dt.to_s
=> "2012-07-01T00:00:00+02:00"
irb(main):007:0> dt.zone
=> "+02:00"
irb(main):008:0> dt.dst?
NoMethodError: undefined method `dst?' for #<DateTime:0x007f34ea6c3cb8>

Może to powodować problemy, gdy czasy są wprowadzane jako czas letni, a następnie konwertowane na strefę czasową inną niż czas letni, bez śledzenia prawidłowych przesunięć poza DateTimesobą (wiele systemów operacyjnych może już to załatwić).

Ogólnie rzecz biorąc, powiedziałbym, że obecnie Timejest lepszym wyborem dla większości aplikacji.

Zwróć też uwagę na istotną różnicę przy dodawaniu: kiedy dodajesz liczbę do obiektu Time, jest ona liczona w sekundach, ale kiedy dodajesz liczbę do DateTime, jest ona liczona w dniach.

Niels Ganser
źródło
Czy to nadal prawda w 2018 roku?
Qqwy
2
Jest to nadal prawdą w Ruby 2.5.1. ruby-doc.org/stdlib-2.5.1/libdoc/date/rdoc/DateTime.html : „DateTime nie uwzględnia sekund przestępnych, nie śledzi żadnych reguł dotyczących czasu letniego”. Należy jednak pamiętać, że DateTime ma zalety, gdy trzeba poradzić sobie z datami / godzinami kalendarzowymi sprzed okresu gregoriańskiego. Nie wspomniano o tym wcześniej, ale jest to udokumentowane w dokumentacji źródłowej: „[…] Klasa czasu Ruby'ego wdraża proleptyczny kalendarz gregoriański i nie ma pojęcia o reformie kalendarza […]”. Zmodyfikuję swoją odpowiedź, aby podać więcej szczegółów.
Niels Ganser
Timenie ma też pojęcia sekund przestępnych, więc nie różni się od DateTime. Nie wiem, gdzie sprawdziłeś swoje przykłady, ale próbowałem Time.new(2012,6,30,23,59,60,0)w różnych wersjach Rubiego, od 2.0 do 2.7, i zawsze je otrzymywałem 2012-07-01 00:00:00 +0000.
michau
@ michau To, czy Timeobsługuje sekundy przestępne, zależy od konfiguracji systemu operacyjnego i strefy czasowej. Na przykład: TZ=right/UTC ruby -e 'p Time.new(2012,6,30,23,59,60,0)'=> 2012-06-30 23:59:60 +0000While TZ=UTC ruby -e 'p Time.new(2012,6,30,23,59,60,0)'=> 2012-07-01 00:00:00 +0000.
Niels Ganser
@NielsGanser Świetnie, dziękuję! Zasugerowałem edycję, aby wyjaśnić ten punkt.
michau
44

Myślę, że odpowiedź na „jaka jest różnica” jest jedną z niefortunnych odpowiedzi na to pytanie w standardowych bibliotekach Rubiego: dwie klasy / biblioteki zostały stworzone w różny sposób przez różne osoby w różnym czasie. Jest to jedna z niefortunnych konsekwencji społecznościowej natury ewolucji Ruby w porównaniu do starannie zaplanowanego rozwoju czegoś takiego jak Java. Programiści chcą nowej funkcjonalności, ale nie chcą korzystać z istniejących interfejsów API, więc po prostu tworzą nową klasę - dla użytkownika końcowego nie ma oczywistego powodu, aby istnieć.

Dotyczy to ogólnie bibliotek oprogramowania: często powodem, dla którego jakiś kod lub interfejs API jest taki, że okazuje się on historyczny, a nie logiczny.

Pokusa polega na tym, aby zacząć od DateTime, ponieważ wydaje się bardziej ogólny. Data ... i godzina, prawda? Źle. Czas ma również lepsze daty i może analizować strefy czasowe, w których DateTime nie. Również działa lepiej.

Wszędzie używam Czasu.

Jednak dla bezpieczeństwa zwykle pozwalam na przekazywanie argumentów DateTime do moich interfejsów API Timey i konwersję. Również jeśli wiem, że oba mają metodę, którą jestem zainteresowany, akceptuję albo, jak tę metodę, którą napisałem do konwersji czasów na XML (dla plików XMLTV)

# Will take a date time as a string or as a Time or DateTime object and
# format it appropriately for xmtlv. 
# For example, the 22nd of August, 2006 at 20 past midnight in the British Summertime
# timezone (i.e. GMT plus one hour for DST) gives: "20060822002000 +0100"
def self.format_date_time(date_time)
  if (date_time.respond_to?(:rfc822)) then
    return format_time(date_time)
  else 
    time = Time.parse(date_time.to_s)
    return format_time(time)
  end    
end

# Note must use a Time, not a String, nor a DateTime, nor Date.
# see format_date_time for the more general version
def self.format_time(time)
  # The timezone feature of DateTime doesn't work with parsed times for some reason
  # and the timezone of Time is verbose like "GMT Daylight Saving Time", so the only
  # way I've discovered of getting the timezone in the form "+0100" is to use 
  # Time.rfc822 and look at the last five chars
  return "#{time.strftime( '%Y%m%d%H%M%S' )} #{time.rfc822[-5..-1]}"
end
Rabarbar
źródło
8
Ponadto Time.new i DateTime.new inaczej traktują strefę czasową. Jestem na GMT + 7, więc Time.new(2011, 11, 1, 10, 30)produkuje 2011-11-01 10:30:00 +0700podczas DateTime.new(2011, 11, 1, 10, 30)produkcji Tue, 01 Nov 2011 10:30:00 +0000.
Phương Nguyễn
21
Jak wszyscy wiemy, starannie zaplanowany rozwój Javy zaowocował wyłącznie prostymi, logicznymi interfejsami API.
pje
@ PhươngNguyễn: Czy dodasz to jako odpowiedź, abym mógł głosować? To jest właśnie powód, dla którego zdecydowałem się wybrać Time zamiast DateTime.
Rozsądny
@Senseful Przepraszamy, do tej pory nie otrzymałem twojej wiadomości. Ludzie już wyrażają pozytywne opinie na temat mojego komentarza, więc myślę, że tutaj jest fajnie.
Phương Nguyễn
@ PhươngNguyễn Cześć, jakieś pomysły jak / dlaczego ta różnica w strefach czasowych? skąd bierze to przesunięcie?
Joel_Blum
10

Odkryłem, że takie funkcje jak analizowanie i obliczanie początku / końca dnia w różnych strefach czasowych są łatwiejsze do wykonania przy użyciu DateTime, przy założeniu , że używasz rozszerzeń ActiveSupport .

W moim przypadku musiałem obliczyć koniec dnia w strefie czasowej użytkownika (dowolnie) na podstawie czasu lokalnego użytkownika, który otrzymałem jako ciąg znaków, np. „2012-10-10 10:10 +0300”

Dzięki DateTime jest to tak proste

irb(main):034:0> DateTime.parse('2012-10-10 10:10 +0300').end_of_day
=> Wed, 10 Oct 2012 23:59:59 +0300
# it preserved the timezone +0300

Teraz spróbujmy to samo z Time:

irb(main):035:0> Time.parse('2012-10-10 10:10 +0300').end_of_day
=> 2012-10-10 23:59:59 +0000
# the timezone got changed to the server's default UTC (+0000), 
# which is not what we want to see here.

Właściwie Time musi znać strefę czasową przed analizą (pamiętaj też, że to Time.zone.parsenie Time.parse):

irb(main):044:0> Time.zone = 'EET'
=> "EET"
irb(main):045:0> Time.zone.parse('2012-10-10 10:10 +0300').end_of_day
=> Wed, 10 Oct 2012 23:59:59 EEST +03:00

Tak więc w tym przypadku zdecydowanie łatwiej jest korzystać z DateTime.

Jurij Charchenko
źródło
1
Używając tego w produkcji teraz, czy są jakieś wady tej techniki?
Alex Moore-Niemi
1
DateTime nie uwzględnia czasu letniego. Dlatego oszczędzając czas, nie będzie działał poprawnie. DateTime.parse('2014-03-30 01:00:00 +0100').end_of_dayprodukuje Sun, 30 Mar 2014 23:59:59 +0100, ale Time.zone = 'CET'; Time.zone.parse('2014-03-30 01:00:00').end_of_dayprodukuje Sun, 30 Mar 2014 23:59:59 CEST +02:00(CET = + 01: 00, CEST = + 02: 00 - zauważ przesunięcie zmienione). Ale aby to zrobić, potrzebujesz więcej informacji o strefie czasowej użytkownika (nie tylko przesunięcie, ale także to, czy oszczędzasz czas).
Petr 'Bubák' Šedivý
1
Może to oznaczać oczywiste, ale Time.zone.parsejest bardzo przydatne, gdy analizuje się czasy z różnymi strefami - zmusza do zastanowienia się, z której strefy należy korzystać. Czasami Time.find_zone działa jeszcze lepiej.
prusswan
1
@ Petr''Bubák''Šedivý DateTimeprzeanalizował podane przez ciebie przesunięcie, które wynosiło +0100. Nie podałeś Timeprzesunięcia, ale strefa czasowa („CET” nie opisuje przesunięcia, to nazwa strefy czasowej). Strefy czasowe mogą mieć różne przesunięcia w ciągu roku, ale przesunięcie jest przesunięciem i zawsze jest takie samo.
Mecki
4

Zastanów się, jak inaczej obsługują strefy czasowe w niestandardowych instancjach:

irb(main):001:0> Time.new(2016,9,1)
=> 2016-09-01 00:00:00 -0400
irb(main):002:0> DateTime.new(2016,9,1)
=> Thu, 01 Sep 2016 00:00:00 +0000
irb(main):003:0> Time.new(2016,9,1).to_i
=> 1472702400
irb(main):004:0> DateTime.new(2016,9,1).to_i
=> 1472688000

Może to być trudne przy tworzeniu przedziałów czasowych itp.

gr8scott06
źródło
Wydaje się, że dzieje się tak tylko z zwykłym rubinem (tzn. Bez Railsów)
vemv
1

Wygląda na to, że w niektórych przypadkach zachowanie jest bardzo różne:

Time.parse("Ends from 28 Jun 2018 12:00 BST").utc.to_s

„2018-06-28 09:00:00 UTC”

Date.parse("Ends from 28 Jun 2018 12:00 BST").to_time.utc.to_s

„2018-06-27 21:00:00 UTC”

DateTime.parse("Ends from 28 Jun 2018 12:00 BST").to_time.utc.to_s

„2018-06-28 11:00:00 UTC”

Aleksiej Striżak
źródło
To ciekawe spostrzeżenie, ale należy wyjaśnić, dlaczego tak się dzieje. Date(co nie jest zaskakujące) analizuje tylko daty, a kiedy Datekonwertowany jest na Time, zawsze używa północy w lokalnej strefie czasowej jako godziny. Różnica między Timei TimeDatewynika z faktu, że Timenie rozumie BST, co jest zaskakujące, biorąc pod uwagę, że strefami Timeczasowymi zwykle zarządza się bardziej poprawnie (na przykład w odniesieniu do czasu letniego). Więc w tym przypadku DateTimepoprawnie analizuje tylko cały ciąg.
michau
1

Oprócz odpowiedzi Nielsa Gansera możesz rozważyć ten argument:

Zwróć uwagę, że Ruby Style Guide dość wyraźnie określa stanowisko w tej kwestii:

Brak daty i godziny

Nie używaj DateTime, chyba że musisz wziąć pod uwagę historyczną reformę kalendarza - a jeśli to zrobisz, wyraźnie określ argument początkowy, aby jasno określić swoje zamiary.

# bad - uses DateTime for current time
DateTime.now

# good - uses Time for current time
Time.now

# bad - uses DateTime for modern date
DateTime.iso8601('2016-06-29')

# good - uses Date for modern date
Date.iso8601('2016-06-29')

# good - uses DateTime with start argument for historical date
DateTime.iso8601('1751-04-23', Date::ENGLAND)
Paul van Leeuwen
źródło