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 | } |