Java tutorial
/* * Copyright (c) 2018 by Oliver Boehm * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * (c)reated 29.03.2018 by oboehm (ob@oasd.de) */ package de.jfachwert.math; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import de.jfachwert.Fachwert; import de.jfachwert.SimpleValidator; import de.jfachwert.pruefung.NullValidator; import de.jfachwert.pruefung.exception.LocalizedIllegalArgumentException; import org.apache.commons.lang3.StringUtils; import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.WeakHashMap; import java.util.logging.Logger; /** * Die Klasse PackedDecimal dienst zum speicherschonende Speichern von Zahlen. * Sie greift die Idee von COBOL auf, wo es den numerischen Datentyp * "COMPUTATIONAL-3 PACKED" gibt, wo die Zahlen in Halb-Bytes (Nibbles) * abgespeichert wird. D.h. In einem Byte lassen sich damit 2 Zahlen * abspeichern. Diese Praesentation ist auch als BCD (Binary Coded Decimal) * bekannt (s. <a href="https://de.wikipedia.org/wiki/BCD-Code">BCD-Code</a> * in Wikipedia). * <p> * Dieser Datentyp eignet sich damit fuer: * <ul> * <li>Abspeichern grosser Menge von Zahlen, wenn dabei die interne * Speichergroesse relevant ist,</li> * <li>Abspeichern von Zahlen beliebiger Groesse * (Ersatz fuer {@link java.math.BigDecimal},</li> * <li>Abspeichern von Zahlen mit fuehrender Null (z.B. Vorwahl).</li> * </ul> * <p> * Eine noch kompaktere Darstellung (ca. 20%) laesst sich mit der Chen-Ho- oder * Densely-Packed-Decimal-Kodierung (s. * <a href="http://speleotrove.com/decimal/DPDecimal.html">A Summary of Densely Packed Decimal encoding</a>). * Diese kommt hier aber nicht zum Einsatz. Stattdessen kommt der BCD-Algorithmus * zum Einsatz. Dadurch koennen auch weitere Trenn- und Fuell-Zeichen aufgenommen * werden: * </p> * <ul> * <li>Vorzeichen (+, -)</li> * <li>Formattierung ('.', ',')</li> * <li>Leerzeichen</li> * <li>Trennzeichen (z.B. fuer Telefonnummern)</li> * </ul> * <p> * Die einzelnen Werte, die ein Halb-Byte (Nibble) aufnimmt, sind (angelehnt an * <a href="http://acc-gmbh.com/dochtml/Datentypen4.html">COMPUTATIONAL-3 PACKED</a> * in COBOL): * </p> * <pre> * +-----+---+--------------------------------------------------+ * | 0x0 | 0 | Ziffer 0 | * | 0x1 | 1 | Ziffer 1 | * | ... | | | * | 0x9 | 9 | Ziffer 9 | * | 0xA | / | Trennzeichen fuer Brueche | * | 0xB | | Leerzeichen (Blank) | * | 0xC | + | positives Vorzeichen | * | 0xD | - | Leerzeichen (Blank) | * | 0xE | . | Formatzeichen Tausenderstelle (im Deutschen) | * | 0xF | , | Trennung Vorkomma/Nachkommastelle (im Deutschen) | * +-----+---+--------------------------------------------------+ * </pre> * <p> * Damit koennen auch Zeichenketten nachgebildet werden, die strenggenommen * keine Dezimalzahl darstellen, z.B. "+49/811 32 16-8". Dies ist zwar * zulaessig, jedoch duerfen damit keine mathematische Operation angewendet * werden. Ansonsten kann die Klasse ueberall dort eingesetzt werden, wo * auch eine {@link java.math.BigDecimal} verwendet wird. * </p> * <p> * Die API orientiert sich an die API von {@link BigDecimal} und ist auch von * der {@link Number}-Klasse abgeleitet. Allerdings werden noch nicht alle * Methoden von {@link BigDecimal unterstuetzt}. In diesem Fall kann man auf * die Methode {@link #toBigDecimal()} ausweichen. * </p> * <p> * Da diese Klasse eher eine technische als eine fachliche Klasse ist, wurde * die englische Bezeichnung aus COBOL uebernommen. Sie wird von einigen * Fachwert-Klassen intern verwendet, kann aber auch fuer eigene Zwecke * verwendet werden. * </p> * * @author oboehm * @since 0.6 (29.03.2018) */ @JsonSerialize(using = ToStringSerializer.class) public class PackedDecimal extends AbstractNumber implements Fachwert, Comparable<PackedDecimal> { private static final Logger LOG = Logger.getLogger(PackedDecimal.class.getName()); private static final NullValidator VALIDATOR = new NullValidator(); private static final PackedDecimal[] CACHE = new PackedDecimal[10]; private static final WeakHashMap<String, PackedDecimal> WEAK_CACHE = new WeakHashMap<>(); private final byte[] code; static { for (int i = 0; i < CACHE.length; i++) { CACHE[i] = new PackedDecimal(i); } } /** Leere PackedDecimal. */ public static final PackedDecimal EMPTY = new PackedDecimal(""); /** Die Zahl 0. */ public static final PackedDecimal ZERO = CACHE[0]; /** Die Zahl 1. */ public static final PackedDecimal ONE = CACHE[1]; /** Die Zahl 10. */ public static final PackedDecimal TEN = PackedDecimal.of(10); /** * Instanziiert ein PackedDecimal. * * @param zahl Zahl */ public PackedDecimal(long zahl) { this(Long.toString(zahl)); } /** * Instanziiert ein PackedDecimal. * * @param zahl Zahl */ public PackedDecimal(double zahl) { this(Double.toString(zahl)); } /** * Falls man eine {@link BigDecimal} in eine {@link PackedDecimal} wandeln * will, kann man diesen Konstruktor hier verwenden. Besser ist es * allerdings, wenn man dazu {@link #valueOf(BigDecimal)} verwendet. * * @param zahl eine Dezimalzahl */ public PackedDecimal(BigDecimal zahl) { this(zahl.toString()); } /** * Instanziiert ein PackedDecimal. * * @param zahl String aus Zahlen */ public PackedDecimal(String zahl) { this(zahl, VALIDATOR); } /** * Instanziiert ein PackedDecimal. Diesen Konstruktor kann man verwenden, * wenn man mehr einen eigenen Validator zur Pruefung heranziehen will. * * @param zahl String aus Zahlen * @param validator Validator, der die Zahl prueft */ public PackedDecimal(String zahl, SimpleValidator<String> validator) { this.code = asNibbles(validator.validate(zahl)); } /** * Liefert den uebergebenen String als {@link PackedDecimal} zurueck. * Diese Methode ist dem Konstruktor vorzuziehen, da fuer gaengige Zahlen * wie "0" oder "1" immer das gleiche Objekt zurueckgegeben wird. * * @param zahl beliebige long-Zahl * @return Zahl als {@link PackedDecimal} */ public static PackedDecimal valueOf(long zahl) { return valueOf(Long.toString(zahl)); } /** * Da alle anderen Klassen auch eine of-Methode vorweisen, hat auch diese * Klasse eine of-Methode. Ansonsten entspricht dies der valueOf-Methode. * * @param zahl beliebige long-Zahl * @return Zahl als {@link PackedDecimal} * @since 2.0 */ public static PackedDecimal of(long zahl) { return PackedDecimal.valueOf(zahl); } /** * Liefert den uebergebenen String als {@link PackedDecimal} zurueck. * * @param zahl beliebige Zahl * @return Zahl als {@link PackedDecimal} */ public static PackedDecimal valueOf(double zahl) { return valueOf(Double.toString(zahl)); } /** * Da alle anderen Klassen auch eine of-Methode vorweisen, hat auch diese * Klasse eine of-Methode. Ansonsten entspricht dies der valueOf-Methode. * * @param zahl beliebige Zahl * @return Zahl als {@link PackedDecimal} * @since 2.0 */ public static PackedDecimal of(double zahl) { return PackedDecimal.valueOf(zahl); } /** * Liefert den uebergebenen String als {@link PackedDecimal} zurueck. * Diese Methode ist dem Konstruktor vorzuziehen, da fuer gaengige Zahlen * wie "0" oder "1" immer das gleiche Objekt zurueckgegeben wird. * * @param zahl beliebige Zahl * @return Zahl als {@link PackedDecimal} */ public static PackedDecimal valueOf(BigDecimal zahl) { return valueOf(zahl.toString()); } /** * Da alle anderen Klassen auch eine of-Methode vorweisen, hat auch diese * Klasse eine of-Methode. Ansonsten entspricht dies der valueOf-Methode. * * @param zahl beliebige Zahl * @return Zahl als {@link PackedDecimal} * @since 2.0 */ public static PackedDecimal of(BigDecimal zahl) { return PackedDecimal.valueOf(zahl); } /** * Liefert den uebergebenen String als {@link PackedDecimal} zurueck. * * @param bruch beliebiger Bruch * @return Bruch als {@link PackedDecimal} */ public static PackedDecimal valueOf(AbstractNumber bruch) { return valueOf(bruch.toString()); } /** * Da alle anderen Klassen auch eine of-Methode vorweisen, hat auch diese * Klasse eine of-Methode. Ansonsten entspricht dies der valueOf-Methode. * * @param zahl beliebige Zahl * @return Zahl als {@link PackedDecimal} * @since 2.0 */ public static PackedDecimal of(AbstractNumber zahl) { return PackedDecimal.valueOf(zahl); } /** * Liefert den uebergebenen String als {@link PackedDecimal} zurueck. * Diese Methode ist dem Konstruktor vorzuziehen, da fuer gaengige Zahlen * wie "0" oder "1" immer das gleiche Objekt zurueckgegeben wird. * <p> * Im Gegensatz zum String-Konstruktor darf man hier auch 'null' als Wert * uebergeben. In diesem Fall wird dies in {@link #EMPTY} uebersetzt. * </p> * <p> * Die erzeugten PackedDecimals werden intern in einem "weak" Cache * abgelegt, damit bei gleichen Zahlen auch die gleichen PackedDecimals * zurueckgegeben werden. Dies dient vor allem zur Reduktion des * Speicherverbrauchs. * </p> * * @param zahl String aus Zahlen * @return Zahl als {@link PackedDecimal} */ public static PackedDecimal valueOf(String zahl) { String trimmed = StringUtils.trimToEmpty(zahl); if (StringUtils.isEmpty(trimmed)) { return EMPTY; } if ((trimmed.length() == 1 && Character.isDigit(trimmed.charAt(0)))) { return CACHE[Character.getNumericValue(trimmed.charAt(0))]; } else { return WEAK_CACHE.computeIfAbsent(zahl, PackedDecimal::new); } } /** * Da alle anderen Klassen auch eine of-Methode vorweisen, hat auch diese * Klasse eine of-Methode. Ansonsten entspricht dies der valueOf-Methode. * * @param zahl beliebige Zahl * @return Zahl als {@link PackedDecimal} * @since 2.0 */ public static PackedDecimal of(String zahl) { return PackedDecimal.valueOf(zahl); } /** * Liefert true zurueck, wenn die Zahl als Bruch angegeben ist. * * @return true oder false */ public boolean isBruch() { String s = toString(); if (s.contains("/")) { try { Bruch.of(s); return true; } catch (IllegalArgumentException ex) { LOG.fine(s + " is not a fraction: " + ex); return false; } } else { return false; } } /** * Da sich mit {@link PackedDecimal} auch Telefonnummer und andere * Zahlenkombinationen abspeichern lassen, die im eigentlichen Sinn * keine Zahl darstellen, kann man ueber diese Methode abfragen, ob * eine Zahl abespeichdert wurde oder nicht. * * @return true, falls es sich um eine Zahl handelt. */ public boolean isNumber() { String packed = toString().replaceAll(" ", ""); try { new BigDecimal(packed); return true; } catch (NumberFormatException nfe) { LOG.fine(packed + " is not a number: " + nfe); return isBruch(); } } /** * Liefert die Zahl als Bruch zurueck. * * @return Bruch als Zahl */ public Bruch toBruch() { return Bruch.of(toString()); } /** * Liefert die gepackte Dezimalzahl wieder als {@link BigDecimal} zurueck. * * @return gepackte Dezimalzahl als {@link BigDecimal} */ public BigDecimal toBigDecimal() { return new BigDecimal(toString()); } /** * Summiert den uebergebenen Summanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param summand Summand * @return Summe */ public PackedDecimal add(PackedDecimal summand) { if (this.isBruch() || summand.isBruch()) { return add(summand.toBruch()); } else { return add(summand.toBigDecimal()); } } /** * Summiert den uebergebenen Summanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param summand Operand * @return Differenz */ public PackedDecimal add(BigDecimal summand) { BigDecimal summe = toBigDecimal().add(summand); return PackedDecimal.valueOf(summe); } /** * Summiert den uebergebenen Summanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param summand Operand * @return Differenz */ public PackedDecimal add(Bruch summand) { AbstractNumber summe = toBruch().add(summand); return PackedDecimal.valueOf(summe); } /** * Subtrahiert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Summand * @return Summe */ public PackedDecimal subtract(PackedDecimal operand) { if (this.isBruch() || operand.isBruch()) { return subtract(operand.toBruch()); } else { return subtract(operand.toBigDecimal()); } } /** * Subtrahiert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Differenz */ public PackedDecimal subtract(BigDecimal operand) { BigDecimal result = toBigDecimal().subtract(operand); return PackedDecimal.valueOf(result); } /** * Subtrahiert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Differenz */ public PackedDecimal subtract(Bruch operand) { AbstractNumber result = toBruch().subtract(operand); return PackedDecimal.valueOf(result); } /** * Mulitpliziert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Summand * @return Produkt */ public PackedDecimal multiply(PackedDecimal operand) { if (this.isBruch() || operand.isBruch()) { return multiply(operand.toBruch()); } else { return multiply(operand.toBigDecimal()); } } /** * Multipliziert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Produkt */ public PackedDecimal multiply(BigDecimal operand) { BigDecimal produkt = toBigDecimal().multiply(operand); return PackedDecimal.valueOf(produkt); } /** * Multipliziert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Produkt */ public PackedDecimal multiply(Bruch operand) { AbstractNumber produkt = toBruch().multiply(operand); return PackedDecimal.valueOf(produkt); } /** * Dividiert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Ergebnis der Division */ public PackedDecimal divide(PackedDecimal operand) { if (this.isBruch() || operand.isBruch()) { return divide(operand.toBruch()); } else { return divide(operand.toBigDecimal()); } } /** * Dividiert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Ergebnis der Division */ public PackedDecimal divide(Bruch operand) { return multiply(operand.kehrwert()); } /** * Dividiert den uebergebenen Operanden und liefert als Ergebnis eine neue * {@link PackedDecimal} zurueck * * @param operand Operand * @return Ergebnis der Division */ public PackedDecimal divide(BigDecimal operand) { BigDecimal result = toBigDecimal().divide(operand, RoundingMode.HALF_UP); return PackedDecimal.valueOf(result); } /** * Verschiebt den Dezimalpunkt um n Stellen nach links. * * @param n Anzahl Stellen * @return eine neue {@link PackedDecimal} */ public PackedDecimal movePointLeft(int n) { BigDecimal result = toBigDecimal().movePointLeft(n); return PackedDecimal.valueOf(result); } /** * Verschiebt den Dezimalpunkt um n Stellen nach rechts. * * @param n Anzahl Stellen * @return eine neue {@link PackedDecimal} */ public PackedDecimal movePointRight(int n) { BigDecimal result = toBigDecimal().movePointRight(n); return PackedDecimal.valueOf(result); } /** * Setzt die Anzahl der Nachkommastellen. * * @param n z.B. 0, falls keine Nachkommastelle gesetzt sein soll * @param mode Rundungs-Mode * @return eine neue {@link PackedDecimal} */ public PackedDecimal setScale(int n, RoundingMode mode) { BigDecimal result = toBigDecimal().setScale(n, mode); return PackedDecimal.valueOf(result); } private static byte[] asNibbles(String zahl) { char[] chars = (zahl + " ").toCharArray(); byte[] bytes = new byte[(chars.length) / 2]; try { for (int i = 0; i < bytes.length; i++) { int upper = decode(chars[i * 2]); int lower = decode(chars[i * 2 + 1]); bytes[i] = (byte) ((upper << 4) | lower); } } catch (IllegalArgumentException ex) { throw new LocalizedIllegalArgumentException(zahl, "number", ex); } return bytes; } @Override public String toString() { StringBuilder buf = new StringBuilder(); for (byte b : this.code) { buf.append(encode(b >> 4)); buf.append(encode(b & 0x0F)); } return buf.toString().trim(); } private static int decode(char x) { switch (x) { case '0': return 0x0; case '1': return 0x1; case '2': return 0x2; case '3': return 0x3; case '4': return 0x4; case '5': return 0x5; case '6': return 0x6; case '7': return 0x7; case '8': return 0x8; case '9': return 0x9; case '/': return 0xA; case '\t': case ' ': return 0xB; case '+': return 0xC; case '-': return 0xD; case '.': return 0xE; case ',': return 0xF; default: throw new LocalizedIllegalArgumentException(x, "number"); } } private static char encode(int nibble) { switch (0x0F & nibble) { case 0x0: return '0'; case 0x1: return '1'; case 0x2: return '2'; case 0x3: return '3'; case 0x4: return '4'; case 0x5: return '5'; case 0x6: return '6'; case 0x7: return '7'; case 0x8: return '8'; case 0x9: return '9'; case 0xA: return '/'; case 0xB: return ' '; case 0xC: return '+'; case 0xD: return '-'; case 0xE: return '.'; case 0xF: return ','; default: throw new IllegalStateException("internal error"); } } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return this.toString().hashCode(); } /** * Beim Vergleich zweier PackedDecimals werden auch fuehrende Nullen * beruecksichtigt. D.h. '711' und '0711' werden als unterschiedlich * betrachtet. * * @param obj zu vergleichende PackedDedimal * @return true bei Gleichheit * @see Object#equals(Object) */ @Override public boolean equals(Object obj) { return obj instanceof PackedDecimal && this.toString().equals(obj.toString()); } /** * Vergleicht die andere Zahl mit der aktuellen Zahl. * * @param other die andere {@link PackedDecimal}, die verglichen wird. * @return negtive Zahl, falls this < other, 0 bei Gleichheit, ansonsten * positive Zahl. */ @Override public int compareTo(@NotNull PackedDecimal other) { return this.toBruch().compareTo(other.toBruch()); } }