View Javadoc
1   /*
2    * Copyright (C) 2020-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
3    */
4   package de.powerstat.validation.values;
5   
6   
7   import java.util.Objects;
8   
9   import de.powerstat.validation.interfaces.IValueObject;
10  
11  
12  /**
13   * Year.
14   *
15   * Not DSGVO relevant.
16   *
17   * TODO Weeks weeksWithin() = (50, 51,) 52, 53 (CalendarSystem, Country dependend ISO vs US)
18   * TODO min, max
19   */
20  public final class Year implements Comparable<Year>, IValueObject
21   {
22    /**
23     * Unsupported calendar system constant.
24     */
25    private static final String UNSUPPORTED_CALENDAR_SYSTEM = "Unsupported calendar system!";
26  
27    /* *
28     * Cache for singletons.
29     */
30    // private static final Map<NTuple2<CalendarSystems, Long>, Year> CACHE = new WeakHashMap<>();
31  
32    /**
33     * Year of Gregorian calendar reform.
34     *
35     * TODO Country dependend.
36     */
37    private static final long BEFORE_GREGORIAN_YEAR = 1582;
38  
39    /**
40     * Calendar system.
41     */
42    private final CalendarSystems calendarSystem;
43  
44    /**
45     * Year.
46     */
47    private final long year;
48  
49  
50    /**
51     * Constructor.
52     *
53     * @param calendarSystem Calendar system
54     * @param year Year != 0
55     * @throws NullPointerException When calendarSystem is null
56     * @throws IndexOutOfBoundsException When the year is 0
57     */
58    private Year(final CalendarSystems calendarSystem, final long year)
59     {
60      super();
61      Objects.requireNonNull(calendarSystem, "calendarSystem"); //$NON-NLS-1$
62      if (year == 0)
63       {
64        throw new IndexOutOfBoundsException("Year 0 does not exist!"); //$NON-NLS-1$
65       }
66      this.calendarSystem = calendarSystem;
67      this.year = year;
68     }
69  
70  
71    /**
72     * Year factory.
73     *
74     * @param calendarSystem Calendar system
75     * @param year Year != 0
76     * @return Year object
77     */
78    public static Year of(final CalendarSystems calendarSystem, final long year)
79     {
80      /*
81      final NTuple2<CalendarSystems, Long> tuple = NTuple2.of(calendarSystem, year);
82      synchronized (Year.class)
83       {
84        Year obj = Year.CACHE.get(tuple);
85        if (obj != null)
86         {
87          return obj;
88         }
89        obj = new Year(calendarSystem, year);
90        Year.CACHE.put(tuple, obj);
91        return obj;
92       }
93      */
94      return new Year(calendarSystem, year);
95     }
96  
97  
98    /**
99     * Gregorian calendar year factory.
100    *
101    * @param year Year != 0
102    * @return Year object
103    */
104   public static Year of(final long year)
105    {
106     return of(CalendarSystems.GREGORIAN, year);
107    }
108 
109 
110   /**
111    * Gregorian calendar year factory.
112    *
113    * @param value Year != 0 string
114    * @return Year object
115    */
116   public static Year of(final String value)
117    {
118     return of(CalendarSystems.GREGORIAN, Long.parseLong(value));
119    }
120 
121 
122   /**
123    * Returns the value of this Year as an long.
124    *
125    * @return The numeric value represented by this object after conversion to type long.
126    */
127   public long longValue()
128    {
129     return this.year;
130    }
131 
132 
133   /**
134    * Returns the value of this Year as an String.
135    *
136    * @return The numeric value represented by this object after conversion to type String.
137    */
138   @Override
139   public String stringValue()
140    {
141     return String.valueOf(this.year);
142    }
143 
144 
145   /**
146    * Months within year.
147    *
148    * @return Months (12) within year
149    */
150   public static Months monthsWithin()
151    {
152     return Months.of(12);
153    }
154 
155 
156   /**
157    * Is julian calendar leap year.
158    *
159    * @param year Julian calendar year
160    * @return true: is leap year; false otherwise
161    */
162   private static boolean isJulianLeapYear(final long year)
163    {
164     if (year <= 0)
165      {
166       return ((-year) % 4) == 1;
167      }
168     else
169      {
170       return (year % 4) == 0;
171      }
172    }
173 
174 
175   /**
176    * Is gregorian calendar leap year.
177    *
178    * @param year Gregorian calendar year
179    * @return  true: is leap year; false otherwise
180    */
181   private static boolean isGregorianLeapYear(final long year)
182    {
183     return (((year % 4) == 0) && (((year % 100) > 0) || ((year % 400) == 0)));
184    }
185 
186 
187   /**
188    * Calendar system dependent leap year.
189    *
190    * @return true: is leap year; false otherwise
191    * @throws IllegalStateException When an unsupported calendar system is used
192    */
193   public boolean isLeapYear()
194    {
195     switch (this.calendarSystem)
196      {
197       case JULIAN:
198         return isJulianLeapYear(this.year);
199       case GREGORIAN:
200         if (this.year < BEFORE_GREGORIAN_YEAR) // Country dependend
201          {
202           return isJulianLeapYear(this.year);
203          }
204         else
205          {
206           return isGregorianLeapYear(this.year);
207          }
208       default:
209         throw new IllegalStateException(UNSUPPORTED_CALENDAR_SYSTEM);
210      }
211    }
212 
213 
214   /**
215    * Leap year dependent days within year.
216    *
217    * @return Days within year
218    * @throws IllegalStateException When an unsupported calendar system is used
219    */
220   public Days daysWithin()
221    {
222     switch (this.calendarSystem)
223      {
224       case JULIAN:
225         return Days.of(365L + (this.isLeapYear() ? 1 : 0));
226       case GREGORIAN:
227         if (this.year == BEFORE_GREGORIAN_YEAR) // Country dependend
228          {
229           return Days.of(365L - 10); // Country dependend
230          }
231         return Days.of(365L + (this.isLeapYear() ? 1 : 0));
232       default:
233         throw new IllegalStateException(UNSUPPORTED_CALENDAR_SYSTEM);
234     }
235    }
236 
237 
238   /**
239    * Calculate hash code.
240    *
241    * @return Hash
242    * @see java.lang.Object#hashCode()
243    */
244   @Override
245   public int hashCode()
246    {
247     return Objects.hash(this.calendarSystem, this.year);
248    }
249 
250 
251   /**
252    * Is equal with another object.
253    *
254    * @param obj Object
255    * @return true when equal, false otherwise
256    * @see java.lang.Object#equals(java.lang.Object)
257    */
258   @Override
259   public boolean equals(final Object obj)
260    {
261     if (this == obj)
262      {
263       return true;
264      }
265     if (!(obj instanceof Year))
266      {
267       return false;
268      }
269     final Year other = (Year)obj;
270     return this.year == other.year;
271    }
272 
273 
274   /**
275    * Returns the string representation of this Year.
276    *
277    * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
278    *
279    * "Year[calendarSystem=GREGORIAN, year=2020]"
280    *
281    * @return String representation of this Year
282    * @see java.lang.Object#toString()
283    */
284   @Override
285   public String toString()
286    {
287     final var builder = new StringBuilder(28);
288     builder.append("Year[calendarSystem=").append(this.calendarSystem).append(", year=").append(this.year).append(']'); //$NON-NLS-1$
289     return builder.toString();
290    }
291 
292 
293   /**
294    * Compare with another object.
295    *
296    * @param obj Object to compare with
297    * @return 0: equal; 1: greater; -1: smaller
298    * @throws IllegalStateException When the calendarSystems of the two years are not equal
299    * @see java.lang.Comparable#compareTo(java.lang.Object)
300    */
301   @Override
302   public int compareTo(final Year obj)
303    {
304     Objects.requireNonNull(obj, "obj"); //$NON-NLS-1$
305     if (this.calendarSystem.compareTo(obj.calendarSystem) != 0)
306      {
307       throw new IllegalStateException("CalendarSystems are not equal!");
308      }
309     return Long.compare(this.year, obj.year);
310    }
311 
312 
313   /**
314    * Add years to this year.
315    *
316    * @param years Years to add to this year
317    * @return New year after adding the years to this year with same calendarSystem
318    * @throws ArithmeticException In case of an overflow
319    */
320   public Year add(final Years years)
321    {
322     long newYear = Math.addExact(this.year, years.longValue());
323     if ((this.year < 0) && (newYear >= 0))
324      {
325       newYear = Math.incrementExact(newYear); // Because there is no year 0!
326      }
327     return Year.of(this.calendarSystem, newYear);
328    }
329 
330 
331   /**
332    * Subtract years from this year.
333    *
334    * @param years Years to subtract from this year
335    * @return New year after subtracting years from this year with same calendarSystem
336    * @throws ArithmeticException In case of an underflow
337    */
338   public Year subtract(final Years years)
339    {
340     long newYear = Math.subtractExact(this.year, years.longValue());
341     if ((this.year > 0) && (newYear <= 0))
342      {
343       newYear = Math.decrementExact(newYear); // Because there is no year 0!
344      }
345     return Year.of(this.calendarSystem, newYear);
346    }
347 
348 
349   /**
350    * Increment this year.
351    *
352    * @return New year after incrementing this year
353    * @throws ArithmeticException In case of an overflow
354    */
355   public Year increment()
356    {
357     long newYear = Math.incrementExact(this.year);
358     if (this.year == -1)
359      {
360       newYear = Math.incrementExact(newYear); // Because there is no year 0!
361      }
362     return Year.of(newYear);
363    }
364 
365 
366   /**
367    * Decrement this year.
368    *
369    * @return New year after decrement this year
370    * @throws ArithmeticException In case of an overflow
371    */
372   public Year decrement()
373    {
374     long newYear = Math.decrementExact(this.year);
375     if (this.year == 1)
376      {
377       newYear = Math.decrementExact(newYear); // Because there is no year 0!
378      }
379     return Year.of(newYear);
380    }
381 
382  }