Java tutorial
/* * Copyright (c) 2017 by Oliver Boehm * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * (c)reated 12.07.2017 by oboehm (ob@oasd.de) */ package de.jfachwert.rechnung; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import de.jfachwert.Fachwert; import de.jfachwert.pruefung.exception.InvalidValueException; import de.jfachwert.pruefung.exception.LocalizedIllegalArgumentException; import org.apache.commons.lang3.Range; import org.apache.commons.lang3.StringUtils; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.Month; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Locale; import java.util.Map; import java.util.WeakHashMap; /** * Vor allem bei Abonnements oder bei wiederkehrenden Gebuehren findet man * einen Rechnungsmonat auf der Rechnung. Hier ist nur Monat und Jahr relevant. * Entsprechend gibt es auch nur diese Attribute in dieser Klasse. * <p> * Der Gueltigkeitsbereich des Rechnungsjahres liegt ca. zwischen 2700 v. Chr. * (-2700) bis 2700 n. Chr. (+2700), da intern der Monat und das Jahr * speicheroptimiert in 2 Bytes abgelegt wird. Diese duerfte aber fuer die * meisten Faelle ausreichend sein. * </p> * <p> * Mit 0.8 implementiert diese Klasse auch die wichtigsten Methoden von * {@link LocalDate}. Sie kann damit anstatt der {@link LocalDate}-Klasse * eingesetzt werden, wenn Monats-Genauigkeit ausreicht. * </p> * * @author oboehm * @since 0.3.1 (12.07.2017) */ @JsonSerialize(using = ToStringSerializer.class) public class Rechnungsmonat implements Fachwert { private static final Map<Short, Rechnungsmonat> CACHE = new WeakHashMap<>(); private static final Range<Integer> VALID_MONTH_RANGE = Range.between(1, 12); private static final Range<Integer> VALID_YEAR_RANGE = Range.between(0, 9999); private static final String MONTH = "month"; private static final String YEAR = "year"; private final short monate; /** * Der Default-Konstruktor legt einen Rechnungsmonat vom aktuellen Monat * an. */ public Rechnungsmonat() { this(LocalDate.now()); } /** * Erzeugt einen gueltigen Rechnungsmonat anhand des uebergebenen * {@link LocalDate}s. Will man ein Rechnungsmonat ueber ein * {@link java.util.Date} anlegen, muss man es vorher mit * {@link java.sql.Date#toLocalDate()} in ein {@link LocalDate} wandeln. * * @param date Datum */ public Rechnungsmonat(LocalDate date) { this(date.getMonthValue(), date.getYear()); } /** * Erzeugt einen gueltigen Rechnungsmonat. Normalerweise sollte der * Monat als "7/2017" angegeben werden, es werden aber auch andere * Formate wie "Jul-2017" oder "2017-07-14" unterstuetzt. * <p> * Auch wenn "Jul-2017" und andere Formate als gueltiger Rechnungsmonat * erkannt werden, sollte man dies nur vorsichtig einsetzen, da hier mit * Brute-Force einfach nur geraten wird, welches Format es sein koennte. * </p> * * @param monat z.B. "7/2017" fuer Juli 2017 */ public Rechnungsmonat(String monat) { String[] parts = monat.split("/"); if ((parts.length == 2) && isDigit(parts[0]) && isDigit(parts[1])) { this.monate = asMonate(verify(MONTH, parts[0], VALID_MONTH_RANGE), verify(YEAR, parts[1], VALID_YEAR_RANGE)); } else { LocalDate date = toLocalDate(monat); this.monate = asMonate(date.getMonthValue(), date.getYear()); } } private static short asMonate(int monat, int jahr) { return (short) (jahr * 12 + monat - 1); } private Rechnungsmonat(int monate) { this.monate = (short) monate; } /** * Erzeugt einen gueltigen Rechnungsmonat. * * @param monat zwischen 1 und 12 * @param jahr vierstellige Zahl zwischen -2730 und +2730 */ public Rechnungsmonat(int monat, int jahr) { this(monat + "/" + jahr); } /** * Erzeugt einen gueltigen Rechnungsmonat. * * @param monat MOnat * @param jahr vierstellige Zahl */ public Rechnungsmonat(Month monat, int jahr) { this(monat.getValue(), jahr); } /** * Die of-Methode liefert fuer denselben Rechnungsmonata auch dasselbe * Objekt zurueck. D.h. zwei gleiche Rechnungsmonate werden nur einmal * angelegt, wenn sie ueber diese Methode angelegt werden. Das lohnt sich * vor allem dann, wenn man viele gleiche Rechnungsmonate hat und sich den * Overhead eines Objekts sparen will. * * @param date Datum * @return einen Rechnungsmonat */ public static Rechnungsmonat of(LocalDate date) { return of(new Rechnungsmonat(date)); } /** * Die of-Methode liefert fuer denselben Rechnungsmonata auch dasselbe * Objekt zurueck. D.h. zwei gleiche Rechnungsmonate werden nur einmal * angelegt, wenn sie ueber diese Methode angelegt werden. Das lohnt sich * vor allem dann, wenn man viele gleiche Rechnungsmonate hat und sich den * Overhead eines Objekts sparen will. * * @param datum Datum * @return einen Rechnungsmonat */ public static Rechnungsmonat of(String datum) { return of(new Rechnungsmonat(datum)); } /** * Die of-Methode liefert fuer denselben Rechnungsmonata auch dasselbe * Objekt zurueck. D.h. zwei gleiche Rechnungsmonate werden nur einmal * angelegt, wenn sie ueber diese Methode angelegt werden. Das lohnt sich * vor allem dann, wenn man viele gleiche Rechnungsmonate hat und sich den * Overhead eines Objekts sparen will. * * @param monat zwischen 1 und 12 * @param jahr vierstellige Zahl zwischen -2730 und +2730 * @return einen Rechnungsmonat */ public static Rechnungsmonat of(int monat, int jahr) { return of(new Rechnungsmonat(monat, jahr)); } /** * Die of-Methode liefert fuer denselben Rechnungsmonata auch dasselbe * Objekt zurueck. D.h. zwei gleiche Rechnungsmonate werden nur einmal * angelegt, wenn sie ueber diese Methode angelegt werden. Das lohnt sich * vor allem dann, wenn man viele gleiche Rechnungsmonate hat und sich den * Overhead eines Objekts sparen will. * * @param monat Monat * @param jahr vierstellige Zahl zwischen -2730 und +2730 * @return einen Rechnungsmonat */ public static Rechnungsmonat of(Month monat, int jahr) { return of(new Rechnungsmonat(monat, jahr)); } /** * Die of-Methode liefert fuer denselben Rechnungsmonata auch dasselbe * Objekt zurueck. D.h. zwei gleiche Rechnungsmonate werden nur einmal * angelegt, wenn sie ueber diese Methode angelegt werden. Das lohnt sich * vor allem dann, wenn man viele gleiche Rechnungsmonate hat und sich den * Overhead eines Objekts sparen will. * <p> * Diese Methode dient dazu, um ein "ueberfluessige" Rechnungsmonate, die * durch Aufruf anderer Methoden entstanden sind, dem Garbage Collector * zum Aufraeumen zur Verfuegung zu stellen. * </p> * * @param other anderer Rechnungsmonat * @return einen (bereits instanziierten) Rechnungsmonat */ public static Rechnungsmonat of(Rechnungsmonat other) { Short key = other.monate; Rechnungsmonat alreadyCreated = CACHE.get(key); if (alreadyCreated == null) { alreadyCreated = other; CACHE.put(key, other); } return alreadyCreated; } private static LocalDate toLocalDate(String monat) { String normalized = monat.replaceAll("[/.\\s]", "-"); String[] parts = monat.split("-"); if (parts.length == 2) { normalized = "1-" + normalized; } else if (parts.length != 3) { throw new InvalidValueException(monat, MONTH); } try { return LocalDate.parse(normalized); } catch (DateTimeParseException ex) { return guessLocalDate(normalized, ex); } } private static LocalDate guessLocalDate(String monat, DateTimeParseException ex) { String[] datePatterns = { "d-MMM-yyyy", "d-MM-yyyy", "yyyy-MMM-d", "yyyy-MM-d", "MMM-d-yyyy" }; for (String pattern : datePatterns) { for (Locale locale : Locale.getAvailableLocales()) { try { return LocalDate.parse(monat, DateTimeFormatter.ofPattern(pattern, locale)); } catch (DateTimeParseException ignored) { ex.addSuppressed(new IllegalArgumentException( ignored.getMessage() + " / '" + monat + "' does not match '" + pattern + "'")); } } } throw new InvalidValueException(monat, MONTH, ex); } private static int verify(String context, String value, Range<Integer> range) { int number = Integer.parseInt(value); if (!range.contains(number)) { throw new LocalizedIllegalArgumentException(value, context, range); } return number; } private static boolean isDigit(String number) { return StringUtils.isNumeric(number); } /** * Liefert den Monat zurueck. * * @return Zahl zwischen 1 und 12 */ public int getMonat() { return (monate % 12) + 1; } /** * Liefert das Jahr zurueck. * * @return vierstellige Zahl */ public int getJahr() { return monate / 12; } /** * Liefert den Vormonat. * * @return Vormonat */ public Rechnungsmonat getVormonat() { return new Rechnungsmonat(monate - 1); } /** * Liefert den Folgemonat. * * @return Folgemonat */ public Rechnungsmonat getFolgemonat() { return new Rechnungsmonat(monate + 1); } /** * Liefert den gleichen Monat im Vorjahr. * * @return Monat im Vorjahr */ public Rechnungsmonat getVorjahr() { return new Rechnungsmonat(monate - 12); } /** * Liefert den gleichen Monat im Folgejahr. * * @return Monat im Folgejahr */ public Rechnungsmonat getFolgejahr() { return new Rechnungsmonat(monate + 12); } /** * Liefert den ersten Tag eines Rechnungsmonats. * * @return z.B. 1.3.2018 * @since 0.6 */ public LocalDate ersterTag() { return LocalDate.of(getJahr(), getMonat(), 1); } /** * Diese Methode kann verwendet werden, um den ersten Montag im Monat * zu bestimmen. Dazu ruft man diese Methode einfach mit * {@link DayOfWeek#MONDAY} als Parameter auf. * * @param wochentag z.B. {@link DayOfWeek#MONDAY} * @return z.B. erster Arbeitstag * @since 0.6 */ public LocalDate ersterTag(DayOfWeek wochentag) { LocalDate tag = ersterTag(); while (tag.getDayOfWeek() != wochentag) { tag = tag.plusDays(1); } return tag; } /** * Diese Methode liefert den ersten Arbeitstag eines Monats. Allerdings * werden dabei keine Feiertag beruecksichtigt, sondern nur die Wochenende, * die auf einen ersten des Monats fallen, werden berucksichtigt. * * @return erster Arbeitstag * @since 0.6 */ public LocalDate ersterArbeitstag() { LocalDate tag = ersterTag(); switch (tag.getDayOfWeek()) { case SATURDAY: return tag.plusDays(2); case SUNDAY: return tag.plusDays(1); default: return tag; } } /** * Liefert den letzten Tag eines Rechnungsmonats. * * @return z.B. 31.3.2018 * @since 0.6 */ public LocalDate letzterTag() { return getFolgemonat().ersterTag().minusDays(1); } /** * Diese Methode kann verwendet werden, um den letzten Freitag im Monat * zu bestimmen. Dazu ruft man diese Methode einfach mit * {@link DayOfWeek#FRIDAY} als Parameter auf. * * @param wochentag z.B. {@link DayOfWeek#FRIDAY} * @return z.B. letzter Arbeitstag * @since 0.6 */ public LocalDate letzterTag(DayOfWeek wochentag) { LocalDate tag = ersterTag(); while (tag.getDayOfWeek() != wochentag) { tag = tag.minusDays(1); } return tag; } /** * Diese Methode liefert den letzten Arbeitstag eines Monats. Allerdings * werden dabei keine Feiertag beruecksichtigt, sondern nur die Wochenende, * die auf einen letzten des Monats fallen, werden berucksichtigt. * * @return letzter Arbeitstag * @since 0.6 */ public LocalDate letzterArbeitstag() { LocalDate tag = letzterTag(); switch (tag.getDayOfWeek()) { case SATURDAY: return tag.minusDays(1); case SUNDAY: return tag.minusDays(2); default: return tag; } } /** * Liefert das Rechnungsatum als {@link LocalDate} zurueck. Sollte das * Datum als {@link java.util.Date} benoetigt werden, kann man es mit * {@link java.sql.Date#valueOf(LocalDate)} konvertieren. * * @return z.B. 1.7.2017 fuer "7/2017" */ public LocalDate asLocalDate() { return ersterTag(); } /** * Diese Methode liefert den Rechnungsmonat, der um 'yearsToAdd' in der * Zukunft liegt. Sie dient dazu, um den Rechnungsmonat auch als Ersatz * fuer {@link LocalDate} verwenden zu koennen. Deswegen ist der * Methodennamen auf Englisch. * * @param yearsToAdd Anzahl Jahre, die aufaddiert werden * @return neuen Rechnungsmonat * @since 1.0 * @see LocalDate#plusYears(long) */ public Rechnungsmonat plusYears(int yearsToAdd) { return plusMonths(yearsToAdd * 12); } /** * Diese Methode liefert den Monat, der um 'monthsToAdd' in der Zukunft * liegt. Sie dient dazu, um den Rechnungsmonat auch als Ersatz fuer * {@link LocalDate} verwenden zu koennen. Deswegen ist der * Methodennamen auf Englisch. * * @param monthsToAdd Anzahl Monate, die aufaddiert werden * @return neuen Rechnungsmonat * @since 1.0 * @see LocalDate#plusMonths(long) */ public Rechnungsmonat plusMonths(int monthsToAdd) { if (monthsToAdd == 0) { return this; } else { return new Rechnungsmonat(monate + monthsToAdd); } } /** * Diese Methode liefert den Rechnungsmonat, der um 'yeara' zurueck * liegt. Sie dient dazu, um den Rechnungsmonat auch als Ersatz * fuer {@link LocalDate} verwenden zu koennen. Deswegen ist der * Methodennamen auf Englisch. * * @param years Anzahl Jahre, die subtrahiert werden * @return neuen Rechnungsmonat * @since 1.0 * @see LocalDate#minusYears(long) */ public Rechnungsmonat minusYears(int years) { return plusYears(-years); } /** * Diese Methode liefert den Monat, der um 'months' zurueck * liegt. Sie dient dazu, um den Rechnungsmonat auch als Ersatz fuer * {@link LocalDate} verwenden zu koennen. Deswegen ist der * Methodennamen auf Englisch. * * @param months Anzahl Monate, die subtrahiert werden * @return neuen Rechnungsmonat * @since 1.0 * @see LocalDate#minusMonths(long) */ public Rechnungsmonat minusMonths(int months) { return plusMonths(-months); } /** * Hiermit kann der Rechnungsmonats im gewuenschten Format ausgegeben * werden. Als Parameter sind die gleichen Patterns wie beim * {@link DateTimeFormatter#ofPattern(String, java.util.Locale)} bzw. * {@link java.text.SimpleDateFormat} moeglich. * * @param pattern z.B. "MM/yyyy" * @return z.B. "07/2017" */ public String format(String pattern) { return format(pattern, Locale.getDefault()); } /** * Hiermit kann der Rechnungsmonats im gewuenschten Format ausgegeben * werden. Als Parameter sind die gleichen Patterns wie beim * {@link DateTimeFormatter#ofPattern(String, Locale)} bzw. * {@link java.text.SimpleDateFormat} moeglich. * * @param pattern z.B. "MM/yyyy" * @param locale gewuenschte Locale * @return z.B. "07/2017" */ public String format(String pattern, Locale locale) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, locale); return asLocalDate().format(formatter); } /** * Als Hashcode nehmen wir einfach die Nummer des Monats seit Christi * Geburt. * * @return Nummer des Monats seit 1.1.0000 */ @Override public int hashCode() { return this.monate; } /** * Zwei Rechnungsmonat sind gleich, wenn Monat und Jahr gleich sind. * * @param obj Vergleichsmonat * @return true bei Gleichheit */ @Override public boolean equals(Object obj) { if (!(obj instanceof Rechnungsmonat)) { return false; } Rechnungsmonat other = (Rechnungsmonat) obj; return this.monate == other.monate; } /** * Als Ausgabe nehmen wir "7/2017" fuer Juli 2017. * * @return z.B. "7/2017" */ @Override public String toString() { return this.getMonat() + "/" + this.getJahr(); } }