001 package hirondelle.web4j.model;
002
003 import java.util.*;
004 import java.io.Serializable;
005 import java.io.IOException;
006 import java.io.ObjectInputStream;
007 import java.io.ObjectOutputStream;
008 import java.math.BigDecimal;
009 import java.math.RoundingMode;
010 import hirondelle.web4j.util.Util;
011
012 /**
013 Represents a decimal amount.
014
015 <P>Decimal amounts are typically used to represent two kinds of items :
016 <ul>
017 <li>monetary amounts
018 <li>measurements such as temperature, distance, and so on
019 </ul>
020
021 <P>Your applications are not obliged to use this class to represent decimal amounts.
022 You may choose to use {@link BigDecimal} instead (perhaps along with an <tt>Id</tt>
023 to store a currency, if needed).
024
025 <P>This class exists for these reasons :
026 <ul>
027 <li>to allow your code to read at a higher level
028 <li>to simplify calculations beyond what is available from the {@link BigDecimal} class
029 <li>to help you avoid using the floating point types <tt>double</tt> and <tt>float</tt>,
030 which have many <a href='http://www.ibm.com/developerworks/java/library/j-jtp0114/'>pitfalls</a>
031 </ul>
032
033 <P><tt>Decimal</tt> objects are immutable.
034 Many operations return new <tt>Decimal</tt> objects.
035
036 <h3>Currency Is Unspecified</h3>
037 This class can be used to model amounts of money.
038 <P><em>Many will be surprised that this class does not make any reference to currency.</em>
039 The reason for this is adding currency would render this class a poor <em>building block</em>.
040 Building block objects such as <tt>Date</tt>, <tt>Integer</tt>, and so on, are
041 <em>atomic</em>, in the sense of representing a <em>single</em> piece of data.
042 They correspond to a single column in a table, or a single form control. If the currency
043 were included in this class, then it would no longer be atomic, and it could not be
044 treated by WEB4J as any other building block class.
045 However, allowing this class to be treated like any other building block class is
046 highly advantageous.
047
048 <P>If a feature needs to explicitly distinguish between <em>multiple</em> currencies
049 such as US Dollars and Japanese Yen, then a <tt>Decimal</tt> object
050 will need to be paired by the caller with a <em>second</em> item representing the
051 underlying currency (perhaps modeled as an <tt>Id</tt>).
052 See the {@link Currency} class for more information.
053
054 <h3>Number of Decimal Places</h3>
055 To validate the number of decimals in your Model Objects,
056 call the {@link Check#numDecimalsAlways(int)} or {@link Check#numDecimalsMax(int)} methods.
057
058 <P>The {@link #init(RoundingMode, int)} method is called upon startup.
059 It takes a parameter which specifies the number of decimal places.
060 It is used only for rounding the results of <tt>times</tt> and <tt>div</tt> operations.
061 It is <em>not</em> used to validate the number of decimals in
062 items passed to the <tt>Decimal</tt> constructor.
063
064 <h3>Different Numbers of Decimals</h3>
065 <P>As usual, operations can be performed on two items having a different number of decimal places.
066 For example, these operations are valid (using an informal, <em>ad hoc</em> notation) :
067 <PRE>10 + 1.23 = 11.23
068 10.00 + 1.23 = 11.23
069 10 - 1.23 = 8.77
070 (10 > 1.23) => true </PRE>
071 This corresponds to typical user expectations.
072
073 <P>Note that {@link #equals(Object)} is unusual in that it is the only method sensitive to the exact
074 number of decimal places, while {@link #eq(Decimal)} is not. That is,
075 <PRE>10.equals(10.00) => false
076 10.eq(10.00) => true</PRE>
077
078 <h3>Results With 'Extra' Decimal Places</h3>
079 <P>The <tt>times</tt> and <tt>div</tt> operations are different, since the result
080 can have a larger number of decimals than usual. For example, when dealing with US Dollars, the result
081 <PRE>$10.00 x 0.1256 = $1.256</PRE>
082 has more than two decimals. In such cases, <em>this class will round
083 results of multiplication and division</em>, using
084 the setting passed to {@link #init(RoundingMode, int)}.
085 This policy likely conforms to the expectations of most end users.
086 The {@link #times(long)} method is an exception to this rule.
087
088 <P>The {@link #init(RoundingMode, int)} method takes two parameters.
089 One controls the rounding policy, and the other controls the number of decimals to use <em>by default</em>.
090 When the default number of decimals needs to be overridden, you must use the
091 {@link #changeTimesDivDecimals(int)} method.
092
093 <h3>Terse Method Names</h3>
094 Various methods in this class have unusually terse names, such as
095 <tt>lt</tt> for 'less than', and <tt>gt</tt> for 'greater than', and so on.
096 The intent of such names is to improve the legibility of mathematical
097 expressions.
098
099 <P>Example :
100 <PRE> if ( amount.lt(hundred) ) {
101 cost = amount.times(price);
102 }</PRE>
103
104 <h3>Prefer Decimal to Double</h3>
105 The <tt>times</tt> and <tt>div</tt> methods are overloaded to take <tt>int</tt> for
106 round numbers, and <tt>Decimal</tt> or <tt>double</tt> for numbers with a decimal.
107
108 <P>In short, the <tt>double</tt> versions are best suited when using
109 <em>hard-coded</em> values for factors and divisors, while the
110 <tt>Decimal</tt> versions are suited for the (more common) case of using
111 values coming from the database or user input.
112
113 <P>Using <tt>Decimal</tt> is the preferred form, since there are many pitfalls associated
114 with <tt>double</tt>. The <tt>double</tt> form has been retained since it's
115 more convenient for the caller in some cases, and one of the goals of this class is
116 to allow terse mathematical expressions.
117
118 <h3>Extends Number</h3>
119 This class extends {@link Number}. An immediate benefit of this is that it allows JSTL's
120 <tt>fmt</tt> tags to render <tt>Decimal</tt> objects in the usual way.
121 */
122 public final class Decimal extends Number implements Comparable<Decimal>, Serializable {
123
124 /**
125 Set default values for the rounding style, and the maximum number of decimals
126 to use when calculating results of <tt>times</tt> and <tt>div</tt> operations.
127
128 <P>This method is called by the framework upon startup.
129 The recommended rounding style is {@link RoundingMode#HALF_EVEN}, also called
130 <em>banker's rounding</em>. That rounding style introduces the least bias.
131
132 @param aRounding defines how all numbers are rounded by this class. This rounding
133 style is set once, and cannot be overridden for individual <tt>Decimal</tt> objects.
134 @param aNumDecimalsForTimesDiv number of decimals for results of
135 <tt>times</tt> and <tt>div</tt> operations. Must be 0 or more.
136 Taking the example of US Dollars, this setting would usually be '2'.
137 */
138 public static void init(RoundingMode aRounding, int aNumDecimalsForTimesDiv){
139 if ( aNumDecimalsForTimesDiv < 0 ){
140 throw new IllegalArgumentException("Number of decimals for times-div operations must be 0 or more. Value: " + Util.quote(aNumDecimalsForTimesDiv));
141 }
142 ROUNDING = aRounding;
143 TIMES_DIV_DECIMALS = aNumDecimalsForTimesDiv;
144 }
145
146 /** Return the rounding style passed to the <tt>init</tt> method. */
147 public static RoundingMode getRoundingStyle() { return ROUNDING; }
148
149 /** Return the number of decimals passed to the <tt>init</tt> method. */
150 public static int getTimesDivDecimalsDefault() { return TIMES_DIV_DECIMALS; }
151
152 /**
153 Full constructor.
154
155 @param aAmount required, can be positive or negative.
156 Any number of decimals. The value of {@link BigDecimal#scale()} cannot
157 be negative.
158 */
159 public Decimal(BigDecimal aAmount){
160 this(aAmount, TIMES_DIV_DECIMALS);
161 }
162
163 /**
164 Convenience factory method.
165
166 <P>Instead of :
167 <PRE>Decimal decimal = new Decimal(new BigDecimal("100"));</PRE>
168 one may instead use :
169 <PRE>Decimal decimal = Decimal.from("100");</PRE>
170 which is a bit more legible. This is especially useful when you need to define
171 specific minimum and maximum values used in validation.
172 */
173 public static Decimal from(String aAmount){
174 return new Decimal(new BigDecimal(aAmount));
175 }
176
177 /**
178 Override the default number of decimals retained in <tt>times</tt> and <tt>div</tt> operations.
179
180 The <em>default</em> number of decimals retained in <tt>times</tt> and <tt>div</tt>
181 operations is set during the call to {@link #init(RoundingMode, int)}.
182 The <tt>changeTimesDivDecimals</tt> method is called when that default is not
183 appropriate.
184
185 <P>This method will often be used when modeling physical measurements
186 such as temperature, distance, and so on, where the number of decimals can
187 vary according to context.
188 */
189 public Decimal changeTimesDivDecimals(int aTimesDivDecimals){
190 return new Decimal(fAmount, aTimesDivDecimals);
191 }
192
193 /** Return the amount passed to the constructor. */
194 public BigDecimal getAmount() { return fAmount; }
195
196 /**
197 Return the number of decimals to be retained by <tt>times</tt> and <tt>div</tt> operations.
198
199 <P>See {@link #changeTimesDivDecimals(int)} for a way of altering this value from the default.
200 */
201 public int getTimesDivDecimals() { return fTimesDivDecimals; }
202
203 /** The suffix is needed to distinguish from the public field. Declared 'early' since compiler complains.*/
204 private static final BigDecimal ZERO_BD = BigDecimal.ZERO;
205
206 /**
207 Zero <tt>Decimal</tt> amount.
208
209 <P>Like {@link BigDecimal#ZERO}, this item has no explicit decimal.
210 In most cases that will not matter, since only the {@link #equals(Object)} method is sensitive to
211 exact decimals. All other methods, including {@link #eq(Decimal)}, are not sensitive to exact decimals.
212 */
213 public static final Decimal ZERO = new Decimal(ZERO_BD);
214
215 /**
216 Return the number of decimals in this value.
217
218 <P>For validating the number of decimals in user input, you are highly encouraged to
219 use {@link Check#numDecimalsAlways(int)} or {@link Check#numDecimalsMax(int)}.
220 */
221 public int getNumDecimals(){
222 return fAmount.scale();
223 }
224
225 /** Return <tt>true</tt> only if the amount is positive. */
226 public boolean isPlus(){
227 return fAmount.compareTo(ZERO_BD) > 0;
228 }
229
230 /** Return <tt>true</tt> only if the amount is negative. */
231 public boolean isMinus(){
232 return fAmount.compareTo(ZERO_BD) < 0;
233 }
234
235 /** Return <tt>true</tt> only if the amount is zero. */
236 public boolean isZero(){
237 return fAmount.compareTo(ZERO_BD) == 0;
238 }
239
240 /**
241 Equals (insensitive to number of decimals).
242
243 <P>That is, <tt>10</tt> and <tt>10.00</tt> are considered equal by this method.
244
245 <P>Return <tt>true</tt> only if the amounts are equal.
246 This method is <em>not</em> synonymous with the <tt>equals</tt> method,
247 since the {@link #equals(Object)} method is sensitive to the exact number of decimal places.
248 */
249 public boolean eq(Decimal aThat) {
250 return compareAmount(aThat) == 0;
251 }
252
253 /**
254 Greater than.
255
256 <P>Return <tt>true</tt> only if 'this' amount is greater than
257 'that' amount.
258 */
259 public boolean gt(Decimal aThat) {
260 return compareAmount(aThat) > 0;
261 }
262
263 /**
264 Greater than or equal to.
265
266 <P>Return <tt>true</tt> only if 'this' amount is
267 greater than or equal to 'that' amount.
268 */
269 public boolean gteq(Decimal aThat) {
270 return compareAmount(aThat) >= 0;
271 }
272
273 /**
274 Less than.
275
276 <P>Return <tt>true</tt> only if 'this' amount is less than
277 'that' amount.
278 */
279 public boolean lt(Decimal aThat) {
280 return compareAmount(aThat) < 0;
281 }
282
283 /**
284 Less than or equal to.
285
286 <P>Return <tt>true</tt> only if 'this' amount is less than or equal to
287 'that' amount.
288 */
289 public boolean lteq(Decimal aThat) {
290 return compareAmount(aThat) <= 0;
291 }
292
293 /**
294 Add <tt>aThat</tt> <tt>Decimal</tt> to this <tt>Decimal</tt>.
295 */
296 public Decimal plus(Decimal aThat){
297 return new Decimal(fAmount.add(aThat.fAmount));
298 }
299
300 /**
301 Subtract <tt>aThat</tt> <tt>Decimal</tt> from this <tt>Decimal</tt>.
302 */
303 public Decimal minus(Decimal aThat){
304 return new Decimal(fAmount.subtract(aThat.fAmount));
305 }
306
307 /**
308 Sum a collection of <tt>Decimal</tt> objects.
309 You are encouraged to use database summary functions
310 whenever possible, instead of this method.
311
312 @param aDecimals collection of <tt>Decimal</tt> objects.
313 If the collection is empty, then a zero value is returned.
314 */
315 public static Decimal sum(Collection<Decimal> aDecimals){
316 Decimal sum = new Decimal(ZERO_BD);
317 for(Decimal decimal : aDecimals){
318 sum = sum.plus(decimal);
319 }
320 return sum;
321 }
322
323 /**
324 Multiply this <tt>Decimal</tt> by an integral factor.
325
326 <P>The number of decimals in the return value is the same as
327 the number of decimals of 'this' <tt>Decimal</tt>. For example,
328 <PRE>10 x 2 = 20
329 10.00 x 2 = 20.00</PRE>
330 This conforms to most user's expectations.
331 This behavior is slightly different from the other <tt>times</tt> methods.
332 */
333 public Decimal times(long aFactor){
334 BigDecimal factor = new BigDecimal(aFactor);
335 BigDecimal newAmount = fAmount.multiply(factor);
336 return new Decimal(newAmount);
337 }
338
339 /**
340 Multiply this <tt>Decimal</tt> by an non-integral factor (having a decimal point).
341
342 <P>The number of decimals of the result is taken from
343 {@link #getTimesDivDecimals()}.
344 */
345 public Decimal times(Decimal aFactor){
346 BigDecimal newAmount = fAmount.multiply(aFactor.getAmount());
347 newAmount = newAmount.setScale(fTimesDivDecimals, ROUNDING);
348 return new Decimal(newAmount);
349 }
350
351 /**
352 Multiply this <tt>Decimal</tt> by an non-integral factor (having a decimal point).
353
354 <P>The number of decimals of the result is taken from
355 {@link #getTimesDivDecimals()}.
356
357 Consider using {@link #times(Decimal)} as the preferred alternative.
358 */
359 public Decimal times(double aFactor){
360 return times(asDecimal(aFactor));
361 }
362
363 /**
364 Divide this <tt>Decimal</tt> by an integral divisor.
365
366 <P>The number of decimals of the result is taken from
367 {@link #getTimesDivDecimals()}.
368 */
369 public Decimal div(long aDivisor){
370 BigDecimal divisor = new BigDecimal(aDivisor);
371 BigDecimal newAmount = fAmount.divide(divisor, fTimesDivDecimals, ROUNDING);
372 return new Decimal(newAmount);
373 }
374
375 /**
376 Divide this <tt>Decimal</tt> by an non-integral divisor.
377
378 <P>The number of decimals of the result is taken from
379 {@link #getTimesDivDecimals()}.
380 */
381 public Decimal div(Decimal aDivisor){
382 BigDecimal newAmount = fAmount.divide(aDivisor.getAmount(), fTimesDivDecimals, ROUNDING);
383 return new Decimal(newAmount);
384 }
385
386 /**
387 Divide this <tt>Decimal</tt> by an non-integral divisor.
388
389 <P>The number of decimals of the result is taken from
390 {@link #getTimesDivDecimalsDefault()}.
391
392 Consider using {@link #div(Decimal)} as the preferred alternative.
393 */
394 public Decimal div(double aDivisor){
395 return div(asDecimal(aDivisor));
396 }
397
398 /** Return the absolute value of the amount. */
399 public Decimal abs(){
400 return isPlus() ? this : times(-1);
401 }
402
403 /** Return the amount x (-1). */
404 public Decimal negate(){
405 return times(-1);
406 }
407
408 /**
409 Round to an integer.
410
411 <P>Uses {@link #getRoundingStyle()}.
412 */
413 public Decimal round(){
414 BigDecimal amount = fAmount.setScale(0, ROUNDING);
415 return new Decimal(amount);
416 }
417
418 /**
419 Round to 0 or more decimal places.
420
421 <P>Uses {@link #getRoundingStyle()}.
422 */
423 public Decimal round(int aNumberOfDecimals){
424 if( aNumberOfDecimals < 0 ){
425 throw new IllegalArgumentException("Number of decimals is negative: " + Util.quote(aNumberOfDecimals));
426 }
427 BigDecimal amount = fAmount.setScale(aNumberOfDecimals, ROUNDING);
428 return new Decimal(amount);
429 }
430
431 /**
432 Renders this <tt>Decimal</tt> in a style suitable for debugging.
433
434 <P>Returns the amount in the format defined by {@link BigDecimal#toPlainString()}.
435 */
436 public String toString(){
437 return fAmount.toPlainString();
438 }
439
440 /**
441 Equals (sensitive to number of decimals).
442
443 <P>That is, <tt>10</tt> and <tt>10.00</tt> are <em>not</em>
444 considered equal by this method.
445
446 <P>This implementation imitates {@link BigDecimal#equals(java.lang.Object)},
447 which is also sensitive to the number of decimals (or 'scale').
448
449 See {@link #eq(Decimal)} as well.
450 */
451 public boolean equals(Object aThat){
452 if (this == aThat) return true;
453 if (! (aThat instanceof Decimal) ) return false;
454 Decimal that = (Decimal)aThat;
455 //the object fields are never null :
456 boolean result = (this.fAmount.equals(that.fAmount) );
457 return result;
458 }
459
460 public int hashCode(){
461 if ( fHashCode == 0 ) {
462 fHashCode = HASH_SEED;
463 fHashCode = HASH_FACTOR * fHashCode + fAmount.hashCode();
464 }
465 return fHashCode;
466 }
467
468 /**
469 Implements the {@link Comparable} interface.
470
471 <P>Recommended that you use the other methods such as {@link #eq(Decimal)}, {@link #lt(Decimal)}, and
472 so on, since they have greater clarity and concision.
473 */
474 public int compareTo(Decimal aThat) {
475 final int EQUAL = 0;
476
477 if ( this == aThat ) return EQUAL;
478
479 //the object field is never null
480 int comparison = this.fAmount.compareTo(aThat.fAmount);
481 if ( comparison != EQUAL ) return comparison;
482
483 return EQUAL;
484 }
485
486 /**
487 Required by {@link Number}.
488
489 <P><em>Use of floating point data is highly discouraged.</em>
490 This method is provided only because it's required by <tt>Number</tt>.
491 */
492 @Override public double doubleValue() {
493 return fAmount.doubleValue();
494 }
495
496 /**
497 Required by {@link Number}.
498
499 <P><em>Use of floating point data is highly discouraged.</em>
500 This method is provided only because it's required by <tt>Number</tt>.
501 */
502 @Override public float floatValue() {
503 return fAmount.floatValue();
504 }
505
506 /** Required by {@link Number}. */
507 @Override public int intValue() {
508 return fAmount.intValue();
509 }
510
511 /** Required by {@link Number}. */
512 @Override public long longValue() {
513 return fAmount.longValue();
514 }
515
516 // PRIVATE //
517
518 /**
519 The decimal amount.
520 Never null.
521 @serial
522 */
523 private BigDecimal fAmount;
524
525 private final int fTimesDivDecimals;
526
527 /**
528 The default rounding style to be used if no currency is passed to the constructor.
529 See {@link BigDecimal}.
530 */
531 private static RoundingMode ROUNDING;
532
533 /**
534 Maximum number of decimals for all monies.
535 This provides a way of rounding results of times and div operations.
536 For example, if a result of a multiplication or division is $10.25366,
537 it will be rounded to $10.25 implicitly by this class (if this setting is '2').
538 */
539 private static int TIMES_DIV_DECIMALS;
540
541 /** @serial */
542 private int fHashCode;
543 private static final int HASH_SEED = 23;
544 private static final int HASH_FACTOR = 37;
545
546 private Decimal(BigDecimal aAmount, int aTimesDivDecimals){
547 fAmount = aAmount;
548 fTimesDivDecimals = aTimesDivDecimals;
549 validateState();
550 }
551
552 /**
553 Determines if a deserialized file is compatible with this class.
554
555 Maintainers must change this value if and only if the new version
556 of this class is not compatible with old versions. See Sun docs
557 for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
558 /serialization/spec/version.doc.html> details. </a>
559
560 Not necessary to include in first version of the class, but
561 included here as a reminder of its importance.
562 */
563 private static final long serialVersionUID = 7526471155622776147L;
564
565 /**
566 Always treat de-serialization as a full-blown constructor, by
567 validating the final state of the de-serialized object.
568 */
569 private void readObject(
570 ObjectInputStream aInputStream
571 ) throws ClassNotFoundException, IOException {
572 //always perform the default de-serialization first
573 aInputStream.defaultReadObject();
574 //defensive copy for mutable date field
575 //BigDecimal is not technically immutable, since its non-final
576 fAmount = new BigDecimal( fAmount.toPlainString() );
577 //ensure that object state has not been corrupted or tampered with maliciously
578 validateState();
579 }
580
581 private void writeObject(ObjectOutputStream aOutputStream) throws IOException {
582 //perform the default serialization for all non-transient, non-static fields
583 aOutputStream.defaultWriteObject();
584 }
585
586 private void validateState(){
587 if( fAmount == null ) {
588 throw new IllegalArgumentException("Amount cannot be null");
589 }
590 if (fAmount.scale() < 0){
591 throw new IllegalArgumentException("Amount has scale that is less than zero: " + Util.quote(fAmount.scale()));
592 }
593 }
594
595 /** Ignores scale: 0 same as 0.00 */
596 private int compareAmount(Decimal aThat){
597 return this.fAmount.compareTo(aThat.fAmount);
598 }
599
600 private Decimal asDecimal(double aDouble){
601 //this is the recommended way of building a BigDecimal from a double.
602 BigDecimal result = BigDecimal.valueOf(aDouble);
603 return new Decimal(result);
604 }
605 }