View Javadoc
1   /*
2    *  jDTAUS Banking API
3    *  Copyright (C) 2005 Christian Schulte
4    *  <cs@schulte.it>
5    *
6    *  This library is free software; you can redistribute it and/or
7    *  modify it under the terms of the GNU Lesser General Public
8    *  License as published by the Free Software Foundation; either
9    *  version 2.1 of the License, or any later version.
10   *
11   *  This library is distributed in the hope that it will be useful,
12   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   *  Lesser General Public License for more details.
15   *
16   *  You should have received a copy of the GNU Lesser General Public
17   *  License along with this library; if not, write to the Free Software
18   *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19   *
20   */
21  package org.jdtaus.banking;
22  
23  import java.lang.ref.Reference;
24  import java.lang.ref.SoftReference;
25  import java.text.DecimalFormat;
26  import java.text.ParseException;
27  import java.text.ParsePosition;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.Map;
31  
32  /**
33   * Unique identifier to a particular bank account at a german bank.
34   * <p>A Kontonummer is a positive integer with a maximum of ten digits.</p>
35   *
36   * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
37   * @version $JDTAUS: Kontonummer.java 8661 2012-09-27 11:29:58Z schulte $
38   */
39  public final class Kontonummer extends Number implements Comparable
40  {
41  
42      /**
43       * Constant for the electronic format of a Kontonummer.
44       * <p>The electronic format of a Kontonummer is a ten digit number with leading zeros omitted (e.g. 6789).</p>
45       */
46      public static final int ELECTRONIC_FORMAT = 4001;
47  
48      /**
49       * Constant for the letter format of a Kontonummer.
50       * <p>The letter format of a Kontonummer is a ten digit number with leading zeros omitted separated by spaces
51       * between the first three digits and the second three digits, the second three digits and the third three digits,
52       * and between the third three digits and the lastdigit (e.g. 123 456 789 0).</p>
53       */
54      public static final int LETTER_FORMAT = 4002;
55  
56      /** Maximum number of digits of a Kontonummer. */
57      public static final int MAX_DIGITS = 10;
58  
59      /** Maximum number of characters of a Kontonummer. */
60      public static final int MAX_CHARACTERS = 13;
61  
62      /** {@code 10^0..10^9}. */
63      private static final double[] EXP10 =
64      {
65          1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000,
66          1000000000
67      };
68  
69      /** Serial version UID for backwards compatibility with 1.0.x classes. */
70      private static final long serialVersionUID = 3117245365189973632L;
71  
72      /** Used to cache instances. */
73      private static volatile Reference cacheReference = new SoftReference( null );
74  
75      /**
76       * German account code.
77       * @serial
78       */
79      private long kto;
80  
81      /**
82       * Creates a new {@code Kontonummer} instance.
83       *
84       * @param accountCode The number to create an instance from.
85       *
86       * @throws IllegalArgumentException if {@code accountCode} is negative, zero or greater than 9999999999.
87       *
88       * @see #checkKontonummer(Number)
89       */
90      private Kontonummer( final Number accountCode )
91      {
92          if ( !Kontonummer.checkKontonummer( accountCode ) )
93          {
94              throw new IllegalArgumentException( accountCode.toString() );
95          }
96  
97          this.kto = accountCode.longValue();
98      }
99  
100     /**
101      * Parses text from a string to produce a {@code Kontonummer}.
102      * <p>The method attempts to parse text starting at the index given by {@code pos}. If parsing succeeds, then the
103      * index of {@code pos} is updated to the index after the last character used (parsing does not necessarily use all
104      * characters up to the end of the string), and the parsed value is returned. The updated {@code pos} can be used to
105      * indicate the starting point for the next call to this method.</p>
106      *
107      * @param accountCode A Kontonummer in either electronic or letter format.
108      * @param pos A {@code ParsePosition} object with index and error index information as described above.
109      *
110      * @return The parsed value, or {@code null} if the parse fails.
111      *
112      * @throws NullPointerException if either {@code accountCode} or {@code pos} is {@code null}.
113      */
114     public static Kontonummer parse( final String accountCode, final ParsePosition pos )
115     {
116         if ( accountCode == null )
117         {
118             throw new NullPointerException( "accountCode" );
119         }
120         if ( pos == null )
121         {
122             throw new NullPointerException( "pos" );
123         }
124 
125         Kontonummer ret = null;
126         boolean sawSpace = false;
127         boolean failed = false;
128         final ParsePosition fmtPos = new ParsePosition( 0 );
129         final int len = accountCode.length();
130         final int startIndex = pos.getIndex();
131         final int maxIndex = startIndex + MAX_CHARACTERS;
132         final StringBuffer digits = new StringBuffer( MAX_DIGITS );
133         int mode = ELECTRONIC_FORMAT;
134         int part = 0;
135         int partStart = 0;
136         int partEnd = 2;
137         int digit = 0;
138         int i = startIndex;
139 
140         for ( ; i < len && i < maxIndex && digits.length() < MAX_DIGITS; i++ )
141         {
142             final char c = accountCode.charAt( i );
143 
144             if ( Character.isDigit( c ) )
145             {
146                 sawSpace = false;
147 
148                 if ( mode == LETTER_FORMAT )
149                 {
150                     if ( digit < partStart || digit > partEnd )
151                     {
152                         failed = true;
153                     }
154                     else
155                     {
156                         digits.append( c );
157                     }
158                 }
159                 else
160                 {
161                     digits.append( c );
162                 }
163 
164                 digit++;
165             }
166             else if ( c == ' ' )
167             {
168                 if ( sawSpace || i == startIndex || ( mode == ELECTRONIC_FORMAT && digit != 3 ) )
169                 {
170                     failed = true;
171                 }
172                 else
173                 {
174                     mode = LETTER_FORMAT;
175                     switch ( part )
176                     {
177                         case 0:
178                             partStart = 3;
179                             partEnd = 5;
180                             break;
181                         case 1:
182                             partStart = 6;
183                             partEnd = 8;
184                             break;
185                         case 2:
186                             partStart = 9;
187                             partEnd = 9;
188                             break;
189                         default:
190                             failed = true;
191                             break;
192                     }
193                     part++;
194 
195                     if ( digit < partStart || digit > partEnd )
196                     {
197                         failed = true;
198                     }
199                 }
200 
201                 sawSpace = true;
202             }
203             else
204             {
205                 failed = true;
206             }
207 
208             if ( failed )
209             {
210                 pos.setErrorIndex( i );
211                 break;
212             }
213         }
214 
215         if ( !failed )
216         {
217             final Number num = new DecimalFormat( "##########" ).parse( digits.toString(), fmtPos );
218 
219             if ( num != null && fmtPos.getErrorIndex() == -1 )
220             {
221                 final String key = num.toString();
222                 ret = (Kontonummer) getCache().get( key );
223 
224                 if ( ret == null )
225                 {
226                     if ( !Kontonummer.checkKontonummer( num ) )
227                     {
228                         pos.setErrorIndex( startIndex );
229                         ret = null;
230                     }
231                     else
232                     {
233                         pos.setIndex( i );
234                         ret = new Kontonummer( num );
235                         getCache().put( key, ret );
236                     }
237                 }
238                 else
239                 {
240                     pos.setIndex( i );
241                 }
242             }
243             else
244             {
245                 pos.setErrorIndex( startIndex );
246             }
247         }
248 
249         return ret;
250     }
251 
252     /**
253      * Parses text from the beginning of the given string to produce a {@code Kontonummer}.
254      * <p>Unlike the {@link #parse(String, ParsePosition)} method this method throws a {@code ParseException} if
255      * {@code accountCode} cannot be parsed or is of invalid length.</p>
256      *
257      * @param accountCode A Kontonummer in either electronic or letter format.
258      *
259      * @return The parsed value.
260      *
261      * @throws NullPointerException if {@code accountCode} is {@code null}.
262      * @throws ParseException if the parse fails or {@code accountCode} is of invalid length.
263      */
264     public static Kontonummer parse( final String accountCode ) throws ParseException
265     {
266         if ( accountCode == null )
267         {
268             throw new NullPointerException( "accountCode" );
269         }
270 
271         Kontonummer kto = (Kontonummer) getCache().get( accountCode );
272         if ( kto == null )
273         {
274             final ParsePosition pos = new ParsePosition( 0 );
275             kto = Kontonummer.parse( accountCode, pos );
276             if ( kto == null || pos.getErrorIndex() != -1 || pos.getIndex() < accountCode.length() )
277             {
278                 throw new ParseException( accountCode, pos.getErrorIndex() != -1 ? pos.getErrorIndex() : pos.getIndex() );
279             }
280             else
281             {
282                 getCache().put( accountCode, kto );
283             }
284         }
285 
286         return kto;
287     }
288 
289     /**
290      * Returns an instance for the Kontonummer identified by the given number.
291      *
292      * @param accountCode A number identifying a Kontonummer.
293      *
294      * @return An instance for {@code accountCode}.
295      *
296      * @throws NullPointerException if {@code accountCode} is {@code null}.
297      * @throws IllegalArgumentException if {@code accountCode} is negative, zero or greater than 9999999999.
298      *
299      * @see #checkKontonummer(Number)
300      */
301     public static Kontonummer valueOf( final Number accountCode )
302     {
303         if ( accountCode == null )
304         {
305             throw new NullPointerException( "accountCode" );
306         }
307 
308         final String key = accountCode.toString();
309         Kontonummer ret = (Kontonummer) getCache().get( key );
310         if ( ret == null )
311         {
312             ret = new Kontonummer( accountCode );
313             getCache().put( key, ret );
314         }
315 
316         return ret;
317     }
318 
319     /**
320      * Parses text from the beginning of the given string to produce a {@code Kontonummer}.
321      * <p>Unlike the {@link #parse(String)} method this method throws an {@code IllegalArgumentException} if
322      * {@code accountCode} cannot be parsed or is of invalid length.</p>
323      *
324      * @param accountCode A Kontonummer in either electronic or letter format.
325      *
326      * @return The parsed value.
327      *
328      * @throws NullPointerException if {@code accountCode} is {@code null}.
329      * @throws IllegalArgumentException if the parse fails or {@code accountCode} is of invalid length.
330      */
331     public static Kontonummer valueOf( final String accountCode )
332     {
333         try
334         {
335             return Kontonummer.parse( accountCode );
336         }
337         catch ( final ParseException e )
338         {
339             throw (IllegalArgumentException) new IllegalArgumentException( accountCode ).initCause( e );
340         }
341     }
342 
343     /**
344      * Checks a given number to conform to a Kontonummer.
345      *
346      * @param accountCode The number to check.
347      *
348      * @return {@code true} if {@code accountCode} is a valid Kontonummer; {@code false} if not.
349      */
350     public static boolean checkKontonummer( final Number accountCode )
351     {
352         boolean valid = accountCode != null;
353 
354         if ( valid )
355         {
356             final long num = accountCode.longValue();
357             valid = num > 0L && num < 10000000000L;
358         }
359 
360         return valid;
361     }
362 
363     /**
364      * Returns this Kontonummer as an int value.
365      *
366      * @return This Kontonummer as an int value.
367      */
368     public int intValue()
369     {
370         return (int) this.kto;
371     }
372 
373     /**
374      * Returns this Kontonummer as a long value.
375      *
376      * @return This Kontonummer as a long value.
377      */
378     public long longValue()
379     {
380         return this.kto;
381     }
382 
383     /**
384      * Returns this Kontonummer as a float value.
385      *
386      * @return This Kontonummer as a float value.
387      */
388     public float floatValue()
389     {
390         return this.kto;
391     }
392 
393     /**
394      * Returns this Kontonummer as a double value.
395      *
396      * @return This Kontonummer as a double value.
397      */
398     public double doubleValue()
399     {
400         return this.kto;
401     }
402 
403     /**
404      * Formats a Kontonummer and appends the resulting text to the given string buffer.
405      *
406      * @param style The style to use ({@code ELECTRONIC_FORMAT} or {@code LETTER_FORMAT}).
407      * @param toAppendTo The buffer to which the formatted text is to be appended.
408      *
409      * @return The value passed in as {@code toAppendTo}.
410      *
411      * @throws NullPointerException if {@code toAppendTo} is {@code null}.
412      * @throws IllegalArgumentException if {@code style} is neither {@code ELECTRONIC_FORMAT} nor {@code LETTER_FORMAT}.
413      *
414      * @see #ELECTRONIC_FORMAT
415      * @see #LETTER_FORMAT
416      */
417     public StringBuffer format( final int style, final StringBuffer toAppendTo )
418     {
419         if ( toAppendTo == null )
420         {
421             throw new NullPointerException( "toAppendTo" );
422         }
423         if ( style != Kontonummer.ELECTRONIC_FORMAT && style != Kontonummer.LETTER_FORMAT )
424         {
425             throw new IllegalArgumentException( Integer.toString( style ) );
426         }
427 
428         final int[] digits = Kontonummer.toDigits( this.kto );
429         for ( int i = digits.length - 1, lastDigit = 0; i >= 0; i-- )
430         {
431             if ( digits[i] != 0 || lastDigit > 0 )
432             {
433                 toAppendTo.append( digits[i] );
434                 lastDigit++;
435             }
436 
437             if ( style == Kontonummer.LETTER_FORMAT && ( lastDigit == 3 || lastDigit == 6 || lastDigit == 9 ) )
438             {
439                 toAppendTo.append( ' ' );
440             }
441         }
442 
443         return toAppendTo;
444     }
445 
446     /**
447      * Formats a Kontonummer to produce a string. Same as
448      * <blockquote>
449      * {@link #format(int, StringBuffer) format<code>(style, new StringBuffer()).toString()</code>}
450      * </blockquote>
451      *
452      * @param style The style to use ({@code ELECTRONIC_FORMAT} or {@code LETTER_FORMAT}).
453      *
454      * @return The formatted string.
455      *
456      * @throws IllegalArgumentException if {@code style} is neither {@code ELECTRONIC_FORMAT} nor {@code LETTER_FORMAT}.
457      *
458      * @see #ELECTRONIC_FORMAT
459      * @see #LETTER_FORMAT
460      */
461     public String format( final int style )
462     {
463         return this.format( style, new StringBuffer() ).toString();
464     }
465 
466     /**
467      * Formats a Kontonummer to produce a string. Same as
468      * <blockquote>
469      * {@link #format(int) kontonummer.format(ELECTRONIC_FORMAT)}
470      * </blockquote>
471      *
472      * @param kontonummer The {@code Kontonummer} instance to format.
473      *
474      * @return The formatted string.
475      *
476      * @throws NullPointerException if {@code kontonummer} is {@code null}.
477      */
478     public static String toString( final Kontonummer kontonummer )
479     {
480         if ( kontonummer == null )
481         {
482             throw new NullPointerException( "kontonummer" );
483         }
484 
485         return kontonummer.format( ELECTRONIC_FORMAT );
486     }
487 
488     /**
489      * Creates an array holding the digits of {@code number}.
490      *
491      * @param number The number to return the digits for.
492      *
493      * @return An array holding the digits of {@code number}.
494      */
495     private static int[] toDigits( final long number )
496     {
497         int i;
498         int j;
499         long subst;
500         final int[] ret = new int[ MAX_DIGITS ];
501 
502         for ( i = MAX_DIGITS - 1; i >= 0; i-- )
503         {
504             for ( j = i + 1, subst = 0L; j < MAX_DIGITS; j++ )
505             {
506                 subst += ret[j] * EXP10[j];
507             }
508             ret[i] = (int) Math.floor( ( number - subst ) / EXP10[i] );
509         }
510 
511         return ret;
512     }
513 
514     /**
515      * Creates a string representing the properties of the instance.
516      *
517      * @return A string representing the properties of the instance.
518      */
519     private String internalString()
520     {
521         return new StringBuffer( 500 ).append( "{accountCode=" ).append( this.kto ).append( '}' ).toString();
522     }
523 
524     /**
525      * Gets the current cache instance.
526      *
527      * @return Current cache instance.
528      */
529     private static Map getCache()
530     {
531         Map cache = (Map) cacheReference.get();
532         if ( cache == null )
533         {
534             cache = Collections.synchronizedMap( new HashMap( 1024 ) );
535             cacheReference = new SoftReference( cache );
536         }
537 
538         return cache;
539     }
540 
541     /**
542      * Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer
543      * as this object is less than, equal to, or greater than the specified object.<p>
544      *
545      * @param o The Object to be compared.
546      * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or greater than
547      * the specified object.
548      *
549      * @throws NullPointerException if {@code o} is {@code null}.
550      * @throws ClassCastException if the specified object's type prevents it from being compared to this Object.
551      */
552     public int compareTo( final Object o )
553     {
554         if ( o == null )
555         {
556             throw new NullPointerException( "o" );
557         }
558         if ( !( o instanceof Kontonummer ) )
559         {
560             throw new ClassCastException( o.getClass().getName() );
561         }
562 
563         int result = 0;
564         final Kontonummer that = (Kontonummer) o;
565 
566         if ( !this.equals( that ) )
567         {
568             result = this.kto > that.kto ? 1 : -1;
569         }
570 
571         return result;
572     }
573 
574     /**
575      * Indicates whether some other object is equal to this one.
576      *
577      * @param o The reference object with which to compare.
578      *
579      * @return {@code true} if this object is the same as {@code o}; {@code false} otherwise.
580      */
581     public boolean equals( final Object o )
582     {
583         boolean equal = o == this;
584 
585         if ( !equal && o instanceof Kontonummer )
586         {
587             equal = this.kto == ( (Kontonummer) o ).kto;
588         }
589 
590         return equal;
591     }
592 
593     /**
594      * Returns a hash code value for this object.
595      *
596      * @return A hash code value for this object.
597      */
598     public int hashCode()
599     {
600         return (int) ( this.kto ^ ( this.kto >>> 32 ) );
601     }
602 
603     /**
604      * Returns a string representation of the object.
605      *
606      * @return A string representation of the object.
607      */
608     public String toString()
609     {
610         return super.toString() + this.internalString();
611     }
612 
613 }