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 }