Dlaczego miesiąc kalendarzowy 0 jest w kalendarzu Java?

300

W java.util.Calendarstyczniu jest zdefiniowany jako miesiąc 0, a nie miesiąc 1. Czy jest jakiś konkretny powód?

Widziałem wielu ludzi zdezorientowanych z tego powodu ...

Stéphane Bonniez
źródło
4
Czy nie jest to szczegół implementacji, ponieważ istnieją stałe JANUARY, LUTY itp.? Klasy dat są wcześniejsze niż odpowiednie wsparcie dla Java Enum.
gnud,
6
Jeszcze bardziej denerwujące - dlaczego istnieje Undecember?
matt b
40
@gnud: Nie, to nie jest szczegół implementacji. To sprawia ból, gdy otrzymujesz liczbę całkowitą w „naturalnej” bazie (tj. Jan = 1) i musisz jej używać z API kalendarza.
Jon Skeet,
1
@matt b: dotyczy kalendarzy innych niż gregoriańskie (kalendarze księżycowe itp.), które mają trzynaście miesięcy. Dlatego najlepiej nie myśleć liczbowo, ale pozwolić, aby Kalendarz przeprowadził lokalizację.
erickson,
7
Argument 13 miesięcy nie ma sensu. Jeśli tak, to dlaczego dodatkowy miesiąc to 0 lub 13?
Quinn Taylor,

Odpowiedzi:

323

To tylko część okropnego bałaganu, jakim jest interfejs API daty / godziny Java. Wypisanie, co jest nie tak, zajęłoby bardzo dużo czasu (i jestem pewien, że nie znam połowy problemów). Wprawdzie praca z datami i godzinami jest trudna, ale i tak jest.

Zrób sobie przysługę i zamiast tego użyj Czas Jody lub ewentualnie JSR-310 .

EDYCJA: Z powodów, dla których - jak zauważono w innych odpowiedziach, może to być spowodowane starymi interfejsami API C lub po prostu ogólnym poczuciem, że wszystko zaczyna się od 0 ... z wyjątkiem tego, że dni zaczynają się od 1, oczywiście. Wątpię, czy ktokolwiek spoza pierwotnego zespołu wdrożeniowego naprawdę mógłby podać powody - ale ponownie zachęcam czytelników, aby nie martwili się tak bardzo o to, dlaczego podjęto złe decyzje, aby spojrzeć na całą gamę paskudności java.util.Calendari znaleźć coś lepszego.

Jeden punkt, który jest na korzyść stosując 0 oparte indeksów jest fakt, że takie rzeczy jak „tablicami nazw” łatwiej:

// I "know" there are 12 months
String[] monthNames = new String[12]; // and populate...
String name = monthNames[calendar.get(Calendar.MONTH)];

Oczywiście kończy się to niepowodzeniem, gdy tylko otrzymasz kalendarz z 13 miesiącami ... ale przynajmniej podany rozmiar to oczekiwana liczba miesięcy.

To nie jest dobry powód, ale to powód ...

EDYCJA: W ramach komentarza żądam pewnych pomysłów na temat tego, co uważam za złe w przypadku daty / kalendarza:

  • Zaskakujące podstawy (1900 jako podstawa roku w Date, co prawda dla przestarzałych konstruktorów; 0 jako podstawa miesiąca w obu)
  • Zmienność - używając niezmienne typy sprawia, że o wiele prostsze do pracy z tym, co naprawdę są skutecznie wartości
  • Niedostateczny zestaw typów: To miło mieć Datei Calendarjak różne rzeczy, ale oddzielenia „lokalny” vs „strefowego” Wartości brakuje, podobnie jak data / czas vs daty vs czas
  • Interfejs API, który prowadzi do brzydkiego kodu z magicznymi stałymi, zamiast wyraźnie nazwanych metod
  • Interfejs API, który jest bardzo trudny do uzasadnienia - wszystkie informacje o tym, kiedy wszystko jest przeliczane itp
  • Zastosowanie konstruktorów bez parametrów do domyślnego „teraz”, co prowadzi do trudnego do przetestowania kodu
  • Date.toString()Realizacja który zawsze wykorzystuje system lokalnej strefy czasowej (to mylić wielu użytkowników przepełnienie stosu przedtem)
Jon Skeet
źródło
14
... a co z nieaktualnymi wszystkimi przydatnymi prostymi metodami Date? Teraz muszę używać tego okropnego obiektu kalendarza w skomplikowany sposób, aby robić rzeczy, które kiedyś były proste.
Brian Knoblauch,
3
@Brian: Czuję twój ból. Znowu Joda Time jest prostsza :) (Współczynnik niezmienności sprawia, że ​​praca jest o wiele przyjemniejsza.)
Jon Skeet
8
Nie odpowiedziałeś na pytanie.
Zeemee,
2
@ user443854: Wymieniłem kilka punktów w edycji - sprawdź, czy to pomoże.
Jon Skeet
2
Jeśli używasz Java 8, możesz porzucić klasę Calendar i przejść do nowego i eleganckiego interfejsu API DateTime . Nowe API zawiera także niezmienny / bezpieczny dla wątków DateTimeFormatter, który stanowi dużą poprawę w stosunku do problematycznego i kosztownego SimpleDateFormat.
ccpizza,
43

Ponieważ robienie matematyki w miesiącach jest znacznie łatwiejsze.

1 miesiąc po grudniu przypada na styczeń, ale aby to normalnie zrozumieć, musisz wziąć numer miesiąca i zrobić matematykę

12 + 1 = 13 // What month is 13?

Wiem! Mogę to szybko naprawić za pomocą modułu 12.

(12 + 1) % 12 = 1

Działa to dobrze przez 11 miesięcy do listopada ...

(11 + 1) % 12 = 0 // What month is 0?

Możesz sprawić, by wszystko to znów działało, odejmując 1 przed dodaniem miesiąca, a następnie zrób swój moduł i na koniec dodaj 1 z powrotem ... czyli obejdź podstawowy problem.

((11 - 1 + 1) % 12) + 1 = 12 // Lots of magical numbers!

Pomyślmy teraz o problemie z miesiącami 0–11.

(0 + 1) % 12 = 1 // February
(1 + 1) % 12 = 2 // March
(2 + 1) % 12 = 3 // April
(3 + 1) % 12 = 4 // May
(4 + 1) % 12 = 5 // June
(5 + 1) % 12 = 6 // July
(6 + 1) % 12 = 7 // August
(7 + 1) % 12 = 8 // September
(8 + 1) % 12 = 9 // October
(9 + 1) % 12 = 10 // November
(10 + 1) % 12 = 11 // December
(11 + 1) % 12 = 0 // January

Wszystkie miesiące pracują tak samo i obejście nie jest konieczne.

arucker
źródło
5
To jest satysfakcjonujące. Przynajmniej to szaleństwo ma pewną wartość!
moljac024
„Dużo magicznych liczb” - nie, to tylko jedna z nich pojawia się dwa razy.
user123444555621,
Cofanie się o miesiąc wciąż jest dość trudne, ponieważ C niefortunnie używa operatora „reszty” zamiast „modułu”. Nie jestem również pewien, jak często trzeba naprawdę uderzać miesiąc bez dostosowania roku, a skrócenie miesięcy o 1-12 nie stanowi problemu z `while (miesiąc> 12) {miesiąc- = 12; rok ++;}
supercat
2
Ponieważ rozsądne funkcje, takie jak DateTime.AddMonths, są zbyt trudne do prawidłowego zaimplementowania w bibliotece lib, musimy wykonać matematykę, którą sami opisaliście ... Mmmmmkay
nsimeonov
8
Nie rozumiem tych pozytywnych opinii - ((11 - 1 + 1) % 12) + 1 = 12to (11 % 12) + 1znaczy, że dla miesięcy 1..12 wystarczy dodać 1 po zrobieniu modulo. Nie wymaga magii.
mfitzp
35

Języki oparte na języku C w pewnym stopniu kopiują język C. tmStruktura (zdefiniowane w time.h) ma pole całkowitą tm_monz (komentarzem) zakresie 0-11.

Języki oparte na języku C rozpoczynają tablice od indeksu 0. Było to wygodne w przypadku wyprowadzania ciągu w tablicy nazw miesięcy, z tm_monindeksem.

stesch
źródło
22

Istnieje wiele odpowiedzi na to pytanie, ale i tak przedstawię swój pogląd na ten temat. Powód tego dziwnego zachowania, jak wspomniano wcześniej, pochodzi z POSIX C, time.hgdzie miesiące były przechowywane w int z zakresu 0-11. Aby wyjaśnić dlaczego, spójrz na to w ten sposób; lata i dni są uważane za liczby w języku mówionym, ale miesiące mają swoje własne nazwy. Ponieważ styczeń jest pierwszym miesiącem, będzie przechowywany jako offset 0, pierwszy element tablicy. monthname[JANUARY]byłoby "January". Pierwszy miesiąc w roku to element tablicy pierwszego miesiąca.

Z drugiej strony liczby dzienne, ponieważ nie mają nazw, przechowywanie ich w liczbach całkowitych jako 0-30 byłoby mylące, dodawało wiele day+1instrukcji dotyczących wyświetlania i, oczywiście, było podatne na wiele błędów.

To powiedziawszy, niespójność jest myląca, szczególnie w javascript (który również odziedziczył tę „cechę”), języku skryptowym, w którym należy go wyodrębnić z dala od języka.

TL; DR : Ponieważ miesiące mają nazwy, a dni miesiąca nie.

bitsel piksel
źródło
1
„miesiące mają imiona, a dni nie”. Słyszałeś kiedyś o „piątku”? ;) OK Zgaduję, że miałeś na myśli „.. dni miesiąca nie” - może opłaca się edytować (poza tym dobrą) odpowiedź. :-)
Andrew Thompson
Czy 0/0/0000 lepiej renderuje się jako „00-sty-0000” czy jako „00-XXX-0000”? IMHO, dużo kodu byłoby czystsze, gdyby było trzynaście „miesięcy”, ale miesiąc 0 otrzymałby fałszywą nazwę.
supercat
1
to ciekawe ujęcie, ale 0/0/0000 nie jest prawidłową datą. jak renderowałbyś 40/40/0000?
Piksel Bitworks
12

W Javie 8 jest nowy API JSR 310 Data / Czas, który jest bardziej rozsądny. Lead spec jest taki sam jak główny autor JodaTime i mają wiele podobnych koncepcji i wzorców.

Alex Miller
źródło
2
Nowy interfejs API Data i godzina jest teraz częścią Java 8
mschenk74,
9

Powiedziałbym lenistwo. Tablice zaczynają się od 0 (wszyscy o tym wiedzą); miesiące w roku są tablicą, co prowadzi mnie do przekonania, że ​​jakiś inżynier w firmie Sun po prostu nie zadał sobie trudu, aby wprowadzić ten jeden drobiazg do kodu Java.

Smerf
źródło
9
Nie zrobiłbym tego. Ważniejsze jest optymalizowanie wydajności swoich klientów niż programistów. Ponieważ ten klient spędza tu czas pytając, nie udało mu się.
TheSmurf,
2
Jest to całkowicie niezwiązane z wydajnością - nie jest tak, jakby miesiące były przechowywane w tablicy, a potrzebujesz 13, aby reprezentować 12 miesięcy. Chodzi o to, aby interfejs API nie był tak przyjazny dla użytkownika, jak powinien. Josh Bloch szmaci się za „Data i kalendarz” w „Skutecznej Javie”. Bardzo niewiele interfejsów API jest perfekcyjnych, a interfejsy API daty i godziny w Javie mają niefortunną rolę jako te, które zostały wygłupione. Takie jest życie, ale nie udawajmy, że ma to coś wspólnego z wydajnością.
Quinn Taylor,
1
Dlaczego więc nie policzyć dni od 0 do 30? To jest po prostu niespójne i niechlujne.
Juangui Jordán
9

Prawdopodobnie dlatego, że „struct tm” C robi to samo.

Paul Tomblin
źródło
8

Ponieważ programiści mają obsesję na punkcie indeksów opartych na 0. OK, jest to nieco bardziej skomplikowane: bardziej sensowne jest, gdy pracujesz z logiką niższego poziomu, aby używać indeksowania opartego na 0. Ale w zasadzie nadal będę trzymać pierwsze zdanie.

Dina
źródło
1
Jest to kolejna z tych idiomów / nawyków, które wykraczają drogę z powrotem do asemblerze lub języku maszynowym, gdzie wszystko odbywa się w zakresie offsetu, a nie indeksami. Notacja tablicowa stała się skrótem do uzyskiwania dostępu do sąsiadujących bloków, zaczynając od offsetu 0.
Ken Gentle
4

Osobiście wziąłem obcość interfejsu API kalendarza Java jako wskazówkę, że muszę oderwać się od gregoriańskiego sposobu myślenia i starać się programować bardziej agnostycznie pod tym względem. W szczególności po raz kolejny nauczyłem się unikać stałych zakodowanych na przykład przez miesiące.

Które z poniższych stwierdzeń jest bardziej prawdopodobne?

if (date.getMonth() == 3) out.print("March");

if (date.getMonth() == Calendar.MARCH) out.print("March");

Ilustruje to jedną rzecz, która trochę mnie denerwuje w Joda Time - może zachęcić programistów do myślenia w kategoriach stałych zakodowanych na stałe. (Ale tylko trochę. To nie tak, że Joda zmusza programistów do złego programowania.)

Paul Brinkley
źródło
1
Ale który schemat bardziej przyprawia cię o ból głowy, gdy nie masz stałej w kodzie - masz wartość, która jest wynikiem wywołania usługi internetowej lub cokolwiek innego.
Jon Skeet,
Oczywiście to wywołanie usługi internetowej powinno również wykorzystywać tę stałą. :-) To samo dotyczy każdego zewnętrznego rozmówcy. Po ustaleniu, że istnieje wiele standardów, potrzeba egzekwowania jednego z nich staje się oczywista. (Mam nadzieję, że zrozumiałem twój komentarz ...)
Paul Brinkley,
3
Tak, powinniśmy egzekwować standard, który stosuje prawie wszystko inne na świecie podczas wyrażania miesięcy - standard oparty na 1.
Jon Skeet,
Kluczowym słowem jest tutaj „prawie”. Oczywiście Jan = 1 itd. Wydaje się naturalny w systemie dat o niezwykle szerokim zastosowaniu, ale dlaczego pozwalamy sobie robić wyjątek od unikania stałych zakodowanych, nawet w tym jednym przypadku?
Paul Brinkley,
3
Ponieważ to ułatwia życie. Po prostu tak. Nigdy nie spotkałem problemu pojedynczego z systemem opartym na 1 miesiącu. Widziałem wiele takich błędów w API Java. Ignorowanie tego, co wszyscy inni na świecie nie ma po prostu sensu.
Jon Skeet,
4

Dla mnie nikt nie wyjaśnia tego lepiej niż mindpro.com :

Gotchas

java.util.GregorianCalendarma o wiele mniej błędów i błędów niż w old java.util.Dateklasie, ale nadal nie jest to piknik.

Gdyby istnieli programiści, kiedy po raz pierwszy zaproponowano czas letni, zawetowaliby go jako szalony i trudny do rozwiązania. W świetle dziennym istnieje zasadnicza dwuznaczność. Jesienią, kiedy cofniesz swoje zegary o godzinę o 2 w nocy, istnieją dwa różne momenty, oba zwane 1:30 czasu lokalnego. Możesz je odróżnić tylko wtedy, gdy podczas czytania zarejestrujesz, czy zamierzałeś zaoszczędzić czas letni, czy standardowy czas.

Niestety nie ma sposobu, aby powiedzieć, GregorianCalendarco zamierzałeś. Aby uniknąć niejednoznaczności, należy podać czas lokalny w atrapie strefy czasowej UTC. Programiści zwykle zamykają oczy na ten problem i mają tylko nadzieję, że nikt nic nie zrobi w tej godzinie.

Błąd Millennium. Błędy nadal nie są poza klasami kalendarza. Nawet w JDK (Java Development Kit) 1.3 występuje błąd z 2001 roku. Rozważ następujący kod:

GregorianCalendar gc = new GregorianCalendar();
gc.setLenient( false );
/* Bug only manifests if lenient set false */
gc.set( 2001, 1, 1, 1, 0, 0 );
int year = gc.get ( Calendar.YEAR );
/* throws exception */

Błąd znika o 7 rano 2001/01/01 dla MST.

GregorianCalendarkontrolowany jest przez gigantyczny stos nietypowych stałych magicznych. Ta technika całkowicie niweczy wszelkie nadzieje na sprawdzenie błędów podczas kompilacji. Na przykład, aby uzyskać miesiąc, którego używasz GregorianCalendar. get(Calendar.MONTH));

GregorianCalendarma GregorianCalendar.get(Calendar.ZONE_OFFSET)oszczędność czasu surowego i dziennego GregorianCalendar. get( Calendar. DST_OFFSET), ale nie ma możliwości uzyskania rzeczywistego przesunięcia strefy czasowej. Musisz zdobyć te dwa osobno i dodać je razem.

GregorianCalendar.set( year, month, day, hour, minute) nie ustawia sekund na 0.

DateFormati GregorianCalendarnie siatkuj poprawnie. Musisz podać kalendarz dwa razy, raz pośrednio jako datę.

Jeśli użytkownik nie skonfigurował poprawnie swojej strefy czasowej, domyślnie po cichu wybierze PST lub GMT.

W GregorianCalendar miesiące są numerowane od stycznia = 0, a nie 1, jak wszyscy inni na planecie. Jednak dni zaczynają się od 1, podobnie jak dni tygodnia z niedzielą = 1, poniedziałek = 2,… sobota = 7. Mimo to DateFormat. parsowanie zachowuje się w tradycyjny sposób, a styczeń = 1.

Edwin Dalorzo
źródło
4

java.util.Month

Java zapewnia inny sposób korzystania z indeksów opartych na 1 przez miesiące. Użyj java.time.Monthwyliczenia. Jeden obiekt jest predefiniowany dla każdego z dwunastu miesięcy. Mają numery przypisane do każdego 1-12 na styczeń-grudzień; zadzwoń getValuepod numer.

Użyj Month.JULY(Daje ci 7) zamiast Calendar.JULY(Daje ci 6).

(import java.time.*;)
Digital_Reality
źródło
3

tl; dr

Month.FEBRUARY.getValue()  // February → 2.

2)

Detale

Odpowiedz przez Jon Skeet jest poprawna.

Teraz mamy nowoczesny zamiennik dla tych kłopotliwych starych klas daty i godziny: klasy java.time .

java.time.Month

Wśród tych klas jest wyliczenie . Wyliczenie przenosi jeden lub więcej predefiniowanych obiektów, obiektów, które są automatycznie tworzone w postaci instancji podczas ładowania klasy. Na mamy kilkanaście takich obiektów, z których każdy otrzymał nazwę: , , , i tak dalej. Każda z nich jest stałą klasową. Możesz używać i przekazywać te obiekty w dowolnym miejscu w kodzie. Przykład:Month MonthJANUARYFEBRUARYMARCHstatic final publicsomeMethod( Month.AUGUST )

Na szczęście mają rozsądną numerację, 1-12, gdzie 1 to styczeń, a 12 to grudzień.

Zdobądź Monthobiekt dla określonego numeru miesiąca (1-12).

Month month = Month.of( 2 );  // 2 → February.

Idąc w innym kierunku, poproś Monthobiekt o numer miesiąca.

int monthNumber = Month.FEBRUARY.getValue();  // February → 2.

Wiele innych przydatnych metod w tej klasie, takich jak znajomość liczby dni w każdym miesiącu . Klasa może nawet wygenerować zlokalizowaną nazwę miesiąca.

Możesz uzyskać zlokalizowaną nazwę miesiąca, w różnych długościach lub skrótach.

String output = 
    Month.FEBRUARY.getDisplayName( 
        TextStyle.FULL , 
        Locale.CANADA_FRENCH 
    );

gorączka

Powinieneś także przekazywać obiekty tego wyliczenia wokół swojej bazy kodu, a nie tylko liczb całkowitych . Takie postępowanie zapewnia bezpieczeństwo typu, prawidłowy zakres wartości i sprawia, że ​​kod jest bardziej samodokumentujący. Zobacz Oracle Tutorial, jeśli nie znasz zaskakująco potężnego narzędzia wyliczania w Javie.

Przydatne mogą być również klasy Yeari YearMonth.


O java.time

Środowisko java.time jest wbudowane w Javę 8 i nowsze wersje . Klasy te kłopotliwe zastąpić stary starszych klas Date-Time, takich jak java.util.Date, .Calendar, i java.text.SimpleDateFormat.

Projekt Joda-Time , teraz w trybie konserwacji , zaleca migrację do java.time.

Aby dowiedzieć się więcej, zobacz samouczek Oracle . I przeszukaj stos przepełnienia dla wielu przykładów i wyjaśnień. Specyfikacja to JSR 310 .

Gdzie można uzyskać klasy java.time?

Projekt ThreeTen-Extra rozszerza java.time o dodatkowe klasy. Ten projekt jest poligonem doświadczalnym dla ewentualnych przyszłych dodatków do java.time. Można znaleźć kilka przydatnych klas tutaj takie jak Interval, YearWeek, YearQuarter, i więcej .

Basil Bourque
źródło
0

Nie jest dokładnie zdefiniowane jako zero samo w sobie, jest zdefiniowane jako Kalendarz. Styczeń. Problem polega na użyciu ints jako stałych zamiast wyliczeń. Kalendarz. Styczeń == 0.

Pål GD
źródło
1
Wartości są takie same. Interfejsy API mogą również zwracać 0, jest to identyczne ze stałą. Calendar.JANUARY mógł być zdefiniowany jako 1 - o to właśnie chodzi. Wyliczenie byłoby dobrym rozwiązaniem, ale prawdziwe wyliczenia nie zostały dodane do języka aż do Javy 5, a Date istnieje od samego początku. To niefortunne, ale tak naprawdę nie można „naprawić” tak podstawowego API, gdy użyje go kod innej firmy. Najlepsze, co można zrobić, to zapewnić nowy interfejs API i wycofać stary, aby zachęcić ludzi do przejścia. Dziękuję, Java 7 ...
Quinn Taylor,
0

Ponieważ pisanie języków jest trudniejsze niż się wydaje, a zwłaszcza radzenie sobie z czasem jest o wiele trudniejsze niż większość ludzi myśli. Niewielką część problemu (w rzeczywistości nie Java) można znaleźć na wideo na YouTube „Problem z czasem i strefami czasowymi - Computerphile” na stronie https://www.youtube.com/watch?v=-5wpm-gesOY . Nie zdziw się, jeśli twoja głowa spadnie ze śmiechu w dezorientacji.

Tihamer
źródło
-1

Oprócz odpowiedzi lenistwa DannySmurfa dodam, że ma to zachęcić cię do używania stałych, takich jak Calendar.JANUARY.

Władca
źródło
5
Wszystko to bardzo dobrze, gdy piszesz kod na konkretny miesiąc, ale to jest ból, kiedy masz miesiąc w „normalnej” formie z innego źródła.
Jon Skeet,
1
Jest to również ból, gdy próbujesz wydrukować wartość tego miesiąca w określony sposób - zawsze dodajesz do niej 1.
Brian Warshaw,
-2

Ponieważ wszystko zaczyna się od 0. Jest to podstawowy fakt programowania w Javie. Jeśli jedna rzecz miałaby się od tego odstąpić, doprowadziłoby to do całego zamieszania. Nie kłóćmy się o ich tworzenie i kodowanie z nimi.

Syrrus
źródło
2
Nie, większość rzeczy w świecie rzeczywistym zaczyna się od 1. Przesunięcia zaczynają się od 0, a miesiąc nie jest przesunięciem, jest jednym z dwunastu, podobnie jak dzień miesiąca jest jednym z 31, 30 lub 29 lub 28. Traktowanie miesiąca jako offsetu jest po prostu kapryśne, zwłaszcza jeśli jednocześnie nie traktujemy dnia miesiąca w ten sam sposób. Jaki byłby powód tej różnicy?
SantiBailors
w świecie rzeczywistym zacznij od 1, w świecie Java zacznij od 0. ALE ... Myślę, że to dlatego, że: - aby obliczyć dzień tygodnia, nie można go zrównoważyć dla kilku obliczeń bez dodania jeszcze kilku kroków do to ... - dodatkowo pokazuje pełne dni w miesiącu, jeśli to konieczne (bez zamieszania lub konieczności sprawdzania lutego). - W tym miesiącu zmusza cię do wydruku w formacie daty, którego należy użyć w obu przypadkach. Dodatkowo, ponieważ liczba miesięcy w roku jest regularna, a dni w miesiącu nie ma sensu, jeśli musisz deklarować tablice i używać przesunięcia, aby lepiej dopasować tablicę.
Syrrus,