decimal.java
/*
** Module : decimal.java
** Abstract : Progress 4GL compatible decimal object
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------Description-----------------------------
** 001 GES 20050517 @21250 Created initial version with full support
** for all Progress decimal processing including
** some functions like decimal, minimum and
** maximum.
** 002 GES 20050607 @21442 Minor additions for function support.
** 003 GES 20050614 @21490 Added increment and decrement methods.
** 004 GES 20050616 @21504 Modified intValue() and longValue() to round
** before return.
** 005 GES 20050621 @21532 Added an assign() method to modify this
** instance's state based on the result of
** an expression of compatible type.
** 006 GES 20050721 @21764 Fixed extra leading space issue in
** toStringExport(). Merged cloning support
** for SVG.
** 007 GES 20050822 @22193 Added Undoable interface support.
** 008 GES 20050826 @22259 Implemented use of a BigDecimal in certain
** cases where the faster home-grown algorithm
** in truncateWorker() is limited. Fixed
** intValue() to throw an error when the value
** cannot be fit into an integer.
** 009 GES 20050828 @22278 Added support for temporarily setting
** precision with an automatic reset at the
** end of the scope in which the setting was
** made.
** 010 GES 20050829 @22283 Split assignment of configuration and state
** from normal assign(). deepAssign() is now
** provided to set both configuration and state
** at the same time. Also resolved a problem
** where assign() did not properly round the
** assigned data.
** 011 GES 20050829 @22364 Added processing to generate a string of all
** '?' when there is a negative num that has
** no sign char in the format string OR when a
** left side overflow has occurred.
** 012 GES 20050830 @22383 Added error condition processing.
** 013 ECF 20051004 @22969 Implement hashCode() method. Required to be
** consistent with BaseDataType's implementation
** of equals() method.
** 014 ECF 20051104 @23238 Added iterate() method to PrecisionResetter
** inner class. No-op implementation; required
** to satisfy expanded Finalizable interface.
** 015 GES 20060123 @24023 Implemented support for decimal and group
** separator overrides.
** 016 GES 20060206 @24299 Made compareTo() safe to process any object
** type (avoids ClassCastException).
** 017 GES 20060413 @25521 Added static method to set precision of an
** entire array.
** 018 GES 20060417 @25554 Added default value generation.
** 019 GES 20060517 @26193 Match new Finalizable interface requirement.
** 020 GES 20060724 @28160 Added a forced assign() abstract method.
** 021 GES 20060807 @28473 Moved to externalizable interface to optimize
** network performance.
** 022 ECF 20060812 @28587 Added methods needed for database server-side
** function support.
** 023 GES 20060825 @28899 Added context local default format data.
** 024 GES 20060830 @28990 Added toString signature to expose control
** over right alignment (insertion of padding
** on the left).
** 025 GES 20060920 @29717 Fix for export format with value == 0.
** 026 SIY 20070405 @32826 Part of numbers conversion quirk handling.
** 027 GES 20070427 @33294 Added handling for the case when Java's
** floating point math silently loses least
** significant digits. This is not a foolproof
** approach but it is better than the previous
** one since it reduces the number of
** significant digits that it processes since
** the digits on the left of the decimal point
** don't need any modification so they can be
** ignored. Note that it is still possible that
** there are inexact representations (multiple
** numbers on input are represented as the same
** double on output). This is a core part of the
** design of IEEE 754 floating point upon which
** Java's double is based.
** 028 ECF 20070607 @34012 Replaced internal implementation of double
** with BigDecimal. Certain boundary cases were
** failing with double and required either a
** fixed point decimal implementation or a
** significant rewrite to use a more complicated
** primitive representation (versus a simple
** double). Although the performance is worse
** using BigDecimal, the performance impact of a
** custom solution was unknown and the risk of
** introducing bugs with such a solution was
** high.
** 029 ECF 20070618 @34088 Fixed regression introduced by #028 (@34012).
** Certain methods were incorrectly scaling the
** internal value by powers of 10 when they
** should only have been setting the precision.
** 030 ECF 20070620 @34157 Fixed NPE in hashCode().
** 031 ECF 20070629 @34334 Fixed int/longValue() regression. Code was
** truncating instead of rounding when converting
** to integral values.
** 032 ECF 20071025 @35523 Fixed toStringExport(). A change to the
** parent class to insert a space between a
** leading minus and a decimal point is not
** compatible with the export format, so we fix
** up this anomaly in toStringExport(). Also
** reworked ContextContainer to parameterize
** the ContextLocal superclass.
** 033 EVL 20080415 @37993 Added new method toString() for the case we
** need to transfer negative - sign for the 0.0
** variable value.
** 034 GES 20080417 @38013 Reworked init logic to honor group/dec sep
** overrides that come from the directory. Also
** changed some assignment code to use the
** BigDecimal(double) constructor instead of
** using string parsing.
** 035 EVL 20080418 @38026 Fix for the regression caused by the changes
** from #033. The big decimal bd must be used
** in second call to signum().
** 036 GES 20080911 @39789 Added assign(int) and assign(double)
** signatures.
** 037 CA 20090119 @41173 In some cases, scale was lost in method
** assign(NumberType, boolean) - if the assigned
** value was not a decimal value, the resulting
** BigDecimal was forced to a scale of 10.
** 038 ECF 20090212 @41298 Implemented a faster equals(). Handles most
** common cases without having to invoke the
** superclass' implementation.
** 039 GES 20090424 @41970 Import change.
** 040 CA 20090702 @43025 Added decimal(Date) constructor.
** 041 ECF 20090816 @43663 Removed final modifier from class.
** 042 SVL 20090824 @43723 Fixed NPE into setValue(String).
** 043 SVL 20090908 @43844 Fixed export format.
** 044 ECF 20091030 @44290 Fixed precision (i.e., scale) when unknown value
** is assigned. We were losing our scale value in
** this case, because it was only being stored in
** the internal BigDecimal object, which is nulled
** out to represent unknown value.
** 045 GES 20110303 Moved maximum/minimum to varargs.
** 046 OM 20121220 Added int64 support.
** 047 CA 20130123 Added c'tor which builds a new instance from a
** BaseDataType value, if possible. This should be
** used only when constructing an instance using the
** value returned by a DYNAMIC-FUNCTION call; 4GL
** does some special casting in this case, which is
** not implemented yet.
** 048 CS 20130128 Added conversion support for memptr related
** functions task #1635
** 049 GES 20130322 Fixed the BDT c'tor and modified the assign(BDT,boolean) to be the
** common location for the _POLY morphing. This is safe because all
** current usage would have generated a ClassCastException if that
** method was ever used with an invalid type. Now, instead of the CCE,
** the code will morph the value as is possible. The morphing code
** still needs to be implemented.
** 050 OM 20130403 Fixed maximum/minimum methods when unknown values were returned.
** Fixed compareTo when comparing to int64 objects.
** 051 OM 20130710 Removed unneeded conversion from Date().
** 052 CA 20131001 Eliminated custom group/dec separators when format is used. In this
** case, they must default to comma (,) for group and dot (.) for
** decimal separator.
** 053 CA 20131013 Added no-op deleted() method, required by the changes in Finalizable
** interface.
** 054 GES 20140115 Implemented proper overflow checking and error generation.
** 055 HC 20140213 Added precise conversion to BigDecimal.
** 056 GES 20140131 Eliminated stack overflow issue (infinite recursion). Cleaned up the
** code in isOverflow().
** 057 HC 20140430 Fixed decimal precision for extent variables.
** 058 HC 20140511 Fixed setPrecision behavior - the precision becomes effective after
** the next value assignment.
** 059 MAG 20140602 Fixed the BDT c'tor and modified the assign(BDT,boolean) on _POLY
** morphing. Add BDT types on compareTo(Object).
** Improve compareTo(Object) by comparing unknown values.
** Fixed BiDecimal ONE and ZERO with MAX_SCALE.
** Use BigDecimal.ONE on increment() and decrement() methods.
** 060 OM 20150630 Converting a DATETIME to DECIMAL is an error. New decimal.fromLiteral
** static method for parsing number format independent literals.
** 061 CA 20151222 Added instantiateDefaultExtent.
** 062 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 063 GES 20160617 Added unknown literal processing for the polymorphic assign case.
** 064 ECF 20160614 Implemented faster copy constructor. Fixed constructor which accepts
** a BigDecimal to honor the scale of the argument. Fixed constructor
** which accepts a String and int to set scale even if value is unknown.
** 065 CA 20160627 Reworked undoable support - the undoables register themselves with
** all blocks up the stack, until either 1. the tx block which created
** it is reached or 2. the last tx block where it was saved is reached.
** 066 GES 20171016 Added support for construction from a BigInteger. Added
** fromUnsignedHexLiteral() convenience method.
** 067 GES 20171206 Removed explicit bypass on pending error (new silent error mode).
** 068 CA 20181029 Added a direct-assignment case related to CALL's dynamic invoke, when
** copying data back from an OUTPUT/INPUT-OUTPUT parameter.
** 069 OM 20190206 Added implementation of field size needed by record-length.
** 070 CA 20190526 Disambiguate an assign(Long) call to assign(long).
** 071 CA 20190927 Added support for direct Java access from 4GL code.
** 072 GES 20200520 Added 4GL enum constructor.
** GES 20200605 Fixed lost precision during conversion of enums into decimals.
** ME 20200528 Add assign for "legacy" object - resource id.
** 073 CA 20200608 Fixed equals - must use BigDecimal.compareTo.
** 074 IAS 20201007 Added Type enum
** 075 ME 20210504 Added isIncompatibleTypesOnConversion override
** for invalid POLY conversion.
** AL2 20220319 Added value proxy check in assign.
** TJD 20220504 Java 11 compatibility minor changes
** 076 CA 20230215 'duplicate()' method returns the real type instead of BDT.
** 077 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.util;
import java.io.*;
import java.math.*;
import java.text.*;
import java.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.BaseDataType.*;
/**
* A class that represents a Progress 4GL compatible decimal object whose
* data is mutable. This class provides the basic wrappering of the
* primitive data, instantiation, type conversion, <code>unknown value</code>
* representation and the core comparison logic.
* <p>
* All Progress language features related to integer/decimal data
* types are implemented in the {@link MathOps} class (except comparison
* logic which is in {@link CompareOps}. All Progress integer/decimal
* features are supported including all functionality that can be accessed
* via operators and built-in Progress functions.
* <p>
* Rounding and truncation behavior is completely implemented in this
* class. In particular, assignments do cause a rounding (thus a potential
* loss of precision) but constructors (the equivalent of the Progress
* "init" keyword) do not round even when the precision is set lower than
* the number of significant digits to the right of the decimal point. This
* is how it is done in Progress.
* <p>
* An interesting point: intermediate decimal outputs from functions and
* operators do not inherit the precision (<code>DECIMALS</code>) of the
* operands or parameters. In other words, all returned instances from
* operators or functions will have the default precision of 10.
*/
public class decimal
extends NumberType
{
/** Maximum precision scale applied to any decimal instance */
public static final int MAX_SCALE = 10;
/** Largest 4GL hexadecimal literal that can be represented as a Java signed long. */
public final static decimal MAX_VALID_HEX_LITERAL =
decimal.fromUnsignedHexLiteral("0x7FFFFFFFFFFFFFFF");
/** Smallest 4GL hexadecimal literal that can be represented as a Java signed long. */
public final static decimal MIN_VALID_HEX_LITERAL =
decimal.fromUnsignedHexLiteral("-0x8000000000000000");
/** Constant for BigDecimal value 0 with scale MAX_SCALE */
static final BigDecimal ZERO = BigDecimal.ZERO.setScale(MAX_SCALE);
/** Constant for BigDecimal value 1 with scale MAX_SCALE */
static final BigDecimal ONE = BigDecimal.ONE.setScale(MAX_SCALE);
/** Stores context-local state variables. */
private static ContextContainer work = new ContextContainer();
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(decimal.class);
/**
* Stores the value of this instance. <code>null</code> represents the
* <code>unknown value</code>.
*/
private BigDecimal value = null;
/**
* The number of digits to preserve and to limit to the right of the
* decimal point. Defaults to 10 digits. This is recorded separately from
* {@link #value} to preserve scale when wrapper is set to <code>unknown
* value</code>.
*/
private int scale = MAX_SCALE;
/**
* Default constructor which creates an instance representing the
* <code>unknown value</code>.
*/
public decimal()
{
}
/**
* This is a special c'tor which should be used only when converting the
* value returned by a function or method with polymorphic return type into the
* expected type (i.e. DYNAMIC-FUNCTION()). In such cases, the 4GL does some
* automatic type conversion (see {@link #assign(BaseDataType)}).
*
* @param value
* The value to be used for this instance.
*/
public decimal(BaseDataType value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
value = value == null ? null : value.val();
if (value == null || value.isUnknown())
{
setUnknown();
}
else
{
assign(value);
}
}
/**
* Constructs an instance from a long value and with precision set to its
* maximum. No loss of precision will occur.
* <p>
* It is not possible to yield an <code>unknown value</code> from this
* constructor without calling the method {@link #setUnknown} separately.
*
* @param value
* The value to be used for this instance.
*/
public decimal(long value)
{
this(BigDecimal.valueOf(value).setScale(MAX_SCALE));
}
/**
* Constructs an instance from an int value and with precision set to its
* maximum. No loss of precision will occur.
* <p>
* It is not possible to yield an <code>unknown value</code> from this
* constructor without calling the method {@link #setUnknown} separately.
*
* @param value
* The value to be used for this instance.
*/
public decimal(int value)
{
this((long) value);
}
/**
* Constructs an instance from an int value. No loss of precision will
* occur.
* <p>
* It is not possible to yield an <code>unknown value</code> from this
* constructor without calling the method {@link #setUnknown} separately.
*
* @param value
* The value to be used for this instance.
* @param precision
* The number of digits to maintain to the right of the decimal
* point.
*/
public decimal(int value, int precision)
{
this(BigDecimal.valueOf(value));
setPrecision(precision);
}
/**
* Constructs an instance from an integer value and with precision set to
* its maximum. No loss of precision will occur.
* <p>
* If the parameter's represents the <code>unknown value</code>, this
* instance will also represent the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
*/
public decimal(integer value)
{
deepAssign(value);
}
/**
* Constructs an instance from an int64 value and with precision set to
* its maximum. No loss of precision will occur.
* <p>
* If the parameter's represents the <code>unknown value</code>, this
* instance will also represent the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
*/
public decimal(int64 value)
{
deepAssign(value);
}
/**
* Constructs an instance from a double value and with precision set to
* its maximum. Loss of precision may occur.
* <p>
* If the parameter's value is <code>NaN</code>, the instance will
* represent the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
*/
public decimal(double value)
{
setValue(value);
}
/**
* Constructs an instance from a double value. Loss of precision may
* occur.
* <p>
* If the parameter's value is <code>NaN</code>, the instance will
* represent the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
* @param precision
* The number of digits to maintain to the right of the decimal
* point.
*/
public decimal(double value, int precision)
{
setValue(value);
setPrecision(precision);
}
/**
* Constructs an instance after copying the parameter into the internal
* representation of this class. No loss of precision will occur.
* <p>
* If the parameter's represents the <code>unknown value</code>, this
* instance will also represent the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
*/
public decimal(decimal value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
initialize(value);
return;
}
this.value = value.value;
this.scale = value.scale;
}
/**
* Constructs an instance after converting the string representation of a number into a
* {@code BigDecimal}. Due to rounding, a loss of precision may occur in this conversion (at
* the 10th digit to the right of the decimal point).
* <p>
* This conversion constructor always uses the current number format for parsing. Ex: in the
* default (American) format "3,141" parses as an integer value of 3141, but on European
* (-E Progress command line parameter) it will be parsed as an approximation of PI (3.141).
* For parsing a decimal literal (in American format) regardless of current number format use
* the {@link #fromLiteral(String)}.
*
* @param value
* The value to be used for this instance.
*/
public decimal(String value)
{
this(value, MAX_SCALE);
}
/**
* Constructs an instance after converting the string representation of a number into a
* {@code BigDecimal}. Due to rounding, a loss of precision may occur in this conversion.
* <p>
* This conversion constructor always uses the current number format for parsing. Ex: in the
* default (American) format "3,141" parses as an integer value of 3141, but on European
* (-E Progress command line parameter) it will be parsed as an approximation of PI (3.141).
* For parsing a decimal literal (in American format) regardless of current number format use
* the {@link #fromLiteral(String)}.
*
* @param value
* The value to be used for this instance.
* @param precision
* The number of digits to maintain to the right of the decimal
* point.
*/
public decimal(String value, int precision)
{
// we have to set precision (scale), regardless of unknown value
setPrecision(precision);
// special case: unknown value
if (value == null || value.equals("?"))
{
setUnknown();
return;
}
setValue(value);
}
/**
* Constructs an instance after converting the string representation of a number into a
* {@code BigDecimal}. Due to rounding, a loss of precision may occur in this conversion (at
* the 10th digit to the right of the decimal point).
* <p>
* This conversion constructor always uses the current number format for parsing. Ex: in the
* default (American) format "3,141" parses as an integer value of 3141, but on European
* (-E Progress command line parameter) it will be parsed as an approximation of PI (3.141).
* For parsing a decimal literal (in American format) regardless of current number format use
* the {@link #fromLiteral(String)}.
* <p>
* If the parameter is {@code unknown value} the resulting decimal is the {@code unknown
* value}.
*
* @param value
* The value to be used for this instance.
*/
public decimal(character value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof character)
{
value = (character) value.val();
}
else
{
initialize(value.val());
}
return;
}
if (value.isUnknown())
{
setUnknown();
}
else
{
// special case: unknown value
if (value.getValue().equals("?"))
{
setUnknown();
return;
}
setValue(value.getValue());
setPrecision(MAX_SCALE);
}
}
/**
* Constructs an instance after converting the string representation of a number into a
* {@code BigDecimal}. Due to rounding, a loss of precision may occur in this conversion (at
* the 10th digit to the right of the decimal point).
* <p>
* This conversion constructor always uses the current number format for parsing. Ex: in the
* default (American) format "3,141" parses as an integer value of 3141, but on European
* (-E Progress command line parameter) it will be parsed as an approximation of PI (3.141).
* For parsing a decimal literal (in American format) regardless of current number format use
* the {@link #fromLiteral(String)}.
* <p>
* If the parameter is {@code unknown value} the resulting decimal is the {@code unknown
* value}.
*
* @param value
* The value to be used for this instance.
*/
public decimal(longchar value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof longchar)
{
value = (longchar) value.val();
}
else
{
initialize(value.val());
}
return;
}
// unknown longchar results in 0 decimal value
setValue(value.isUnknown() ? "0" : value.getValue());
setPrecision(MAX_SCALE);
}
/**
* Constructs an instance wth the given enum's value.
* <p>
* If the parameter is {@code unknown value} the resulting instance is the {@code unknown value}.
*
* @param enm
* The enum whose value to be used for this instance.
*/
public decimal(object<? extends LegacyEnum> enm)
{
if (enm.isUnknown())
{
setUnknown();
}
else
{
setValue(enm.ref()._getValue());
}
}
/**
* Constructs an instance after copying the parameter's data into the
* internal representation of this class.
* <p>
* If the parameter's represents the <code>unknown value</code>, this
* instance will also represent the <code>unknown value</code>.
* <p>
* When the parameter is <code>true</code> the resulting decimal value
* will be 1. Otherwise the value is 0.
*
* @param value
* The value to be used for this instance.
*/
public decimal(logical value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof logical)
{
value = (logical) value.val();
}
else
{
initialize(value.val());
}
return;
}
if (!value.isUnknown())
{
this.value = (value.booleanValue() ? ONE : ZERO);
}
}
/**
* Constructs an instance after copying the parameter's data into the
* internal representation of this class.
* <p>
* When the parameter is <code>true</code> the resulting decimal value
* will be 1. Otherwise the value is 0.
*
* @param value
* The value to be used for this instance.
*/
public decimal(boolean value)
{
this.value = (value ? ONE : ZERO);
}
/**
* Constructs an instance after copying the parameter's data into the
* internal representation of this class.
* <p>
* If the parameter represents the <code>unknown value</code>, this
* instance will also represent the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
*/
public decimal(date value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof date)
{
value = (date) value.val();
}
else
{
initialize(value.val());
}
return;
}
if (!value.isUnknown())
{
if (value instanceof datetime)
{
ErrorManager.recordOrShowError(
5729, "Incompatible datatypes found during runtime conversion", false);
}
else
{
this.value = BigDecimal.valueOf(value.longValue());
setPrecision(MAX_SCALE);
}
}
}
/**
* Constructs an instance after copying the parameter's data into the
* internal representation of this class.
* <p>
* If the parameter is <code>null</code>, this instance will represent
* the <code>unknown value</code>.
*
* @param value
* The value to be used for this instance.
*/
public decimal(Number value)
{
if (value instanceof BigInteger)
{
// convert to decimal and then let the normal processing below complete
value = new BigDecimal((BigInteger)value);
}
if (value instanceof BigDecimal)
{
BigDecimal bd = (BigDecimal) value;
this.scale = bd.scale();
setValue(bd);
}
else if (value instanceof Long)
{
setValue(value.longValue());
}
else if (value != null)
{
setValue(value.doubleValue());
}
}
/**
* Parses a Progress decimal literal and returns a {@code decimal} represented.
* Unlike the conversion constructors that always uses the current number format for parsing,
* this utility method will always use the American number format, regardless of current number
* format in use. If you need to convert a language dependent string, use one of the conversion
* constructors.
* <p>
* Ex: the "3,141" will always be parsed as a decimal with integer value of 3141.
* The same string will be parsed by the conversion constructors as 3141 if the default
* (American) number format is in use or as an approximation of PI (3.141) if the European
* (-E Progress command line parameter) format was set.
*
* @param decimalLiteral
* The string to be parsed.
*
* @return the decimal value of the Progress literal represented by the
* {@code decimalLiteral}.
*/
public static decimal fromLiteral(String decimalLiteral)
{
decimal ret = new decimal();
// special case: unknown value
if (decimalLiteral == null || "?".equals(decimalLiteral))
{
return ret; // unknown decimal
}
// this will also check for overflows, so it is safe to assign the result below
String javaStr = parseDecimal(decimalLiteral, '.', ',', false); // literal separators
if (javaStr != null)
{
ret.setValue(new BigDecimal(javaStr).setScale(MAX_SCALE, RoundingMode.HALF_UP));
ret.setPrecision(MAX_SCALE);
}
return ret;
}
/**
* This parses an unsigned hexadecimal whole number and returns a decimal instance that
* represents the exact value. This is needed because the Progress hexadecimal literal
* is an unsigned 64-bit value and the Java hexadecimal literal is a signed 64-bit value.
* By storing the value as a decimal instance, the full range of 4GL values can be
* represented with fidelity.
* <p>
* Input format follows the 4GL hexadecimal literal rules:
* <p>
* <ol>
* <li> Optional prefixed minus sign.
* <li> The characters 0x or 0X.
* <li> Between 0 and 16 (inclusive) case-insensitive hexadecimal characters (0-9, A-F).
* <li> No whitespace or any other characters are allowed.
* <li> If no hexadecimal characters are provided, the value is 0.
* </ol>
*
* @param hex
* The hexadecimal string to be parsed. Must not be <code>null</code>. See above
* for the valid format.
*
* @return The decimal value of the given 4GL hexadecimal literal.
*
* @throws NullPointerException
* If <code>null</code> is passed.
* @throws NumberFormatException
* If the input string is invalid.
*/
public static decimal fromUnsignedHexLiteral(String hex)
{
if (hex == null)
{
throw new NullPointerException("Invalid hexadecimal input.");
}
int len = hex.length();
boolean sign = (hex.charAt(0) == '-');
int max = sign ? 19 : 18;
if (len == 0)
{
throw new NumberFormatException("Empty string input.");
}
if (len > max)
{
throw new NumberFormatException("Invalid chars in hexadecimal input '" + hex + "'");
}
String txt = hex;
if (sign)
{
txt = hex.substring(1);
}
if (!"0x".equalsIgnoreCase(txt.substring(0, 2)))
{
throw new NumberFormatException("Missing 0x/0X in hexadecimal input '" + hex + "'");
}
txt = txt.substring(2);
return new decimal(new BigInteger((sign ? "-" : "") + txt, 16));
}
/**
* Get the type
* @return type
*/
public Type getType()
{
return Type.DECIMAL;
}
/**
* Returns the largest instance from the list. This list may not include {@code null} values.
*
* @param list
* The list of decimals and/or integers to check.
*
* @return The largest instance or {@code unknown value} if any entries in the list are
* {@code unknown} or {@code null} if an empty list is passed.
*/
public static decimal maximum(NumberType ... list)
{
NumberType num = (NumberType) CompareOps.maximum(list);
if (num instanceof decimal)
{
// this includes the particular num.isUnknown() case
return (decimal) num;
}
return num.isUnknown() ? new decimal() : new decimal(num.doubleValue());
}
/**
* Returns the smallest instance from the list. This list may not include
* <code>null</code> values.
*
* @param list
* The list of decimals and/or integers to check.
*
* @return The smallest instance or <code>unknown value</code> if any
* entries in the list are unknown or <code>null</code> if
* an empty list is passed.
*/
public static decimal minimum(NumberType ... list)
{
NumberType num = (NumberType) CompareOps.minimum(list);
if (num instanceof decimal)
{
// this includes the particular num.isUnknown() case
return (decimal) num;
}
return num.isUnknown() ? new decimal() : new decimal(num.doubleValue());
}
/**
* Sets the number of digits that will be maintained to the right of
* the decimal point in each element of the given array. All assignments
* to the value of those elements will be rounded/truncated to this number
* of digits. The value can range between 0 and 10 (inclusive).
* <p>
* The precision will become effective on the next value assignment. I.e.
* the value will not change its precision right after this method is called.
* @param list
* array to be processed
* @param precision
* The precision which must range between 0 and 10 (inclusive).
*/
public static void setPrecision(decimal[] list, int precision)
{
if (precision < 0 || precision > MAX_SCALE)
{
throw new IllegalArgumentException("Invalid precision " + precision + ".");
}
for (decimal item : list)
{
item.setPrecision(precision);
}
// set the decimal precision, for lengths > 0 the precision is stored
// in the array components
if (list.length == 0)
{
ArrayAssigner.setDecimalPrecision(list, precision);
}
}
/**
* Clear any cached formats using the custom group and decimal separators.
*/
static void removeExplicitFormats()
{
WorkArea wa = work.get();
wa.decimalFmt = null;
}
/**
* Get the decimal format, which uses the custom group and decimal separators.
*
* @return See above.
*/
private static DecimalFormat getDecimalFormat()
{
WorkArea wa = work.get();
if (wa.decimalFmt == null)
{
char decSep = getDecimalSeparator();
char groupSep = getGroupSeparator();
// here we must manually override the normal JVM decimal format
// processing otherwise if custom group and decimal seps are defined
// for this context, it would conflict with the default JVM version
DecimalFormatSymbols dfs =
new DecimalFormatSymbols(Locale.getDefault(Locale.Category.FORMAT));
dfs.setDecimalSeparator(decSep);
dfs.setGroupingSeparator(groupSep);
// the pattern must not be localized, use dot as dec separator
wa.decimalFmt = new DecimalFormat("##################.##########", dfs);
}
return wa.decimalFmt;
}
/**
* Dynamically builds the default format string for this given number type
* which honors the given group and decimal separators.
*
* @return The default format string for this type of number.
*/
public String buildDefaultFormat()
{
return "->>" + FORMAT_GROUP_SEP + ">>9" + FORMAT_DEC_SEP + "99";
}
/**
* Returns the default format string for this given number type (which
* honors the given group and decimal separators), building that format
* and caching it if this is the first call in this context.
*
* @return The default format string for this type of number.
*/
public String obtainDefaultFormat()
{
WorkArea wa = work.get();
if (wa.defFmt == null)
{
wa.defFmt = buildDefaultFormat();
}
return wa.defFmt;
}
/**
* Returns the export format string for this given number type which
* honors the given group and decimal separators.
*
* @return The export format string for this type of number or
* <code>null</code> if this type does not use an export
* format.
*/
public String buildExportFormat()
{
return "-" +
StringHelper.repeatChar('>', 50) +
FORMAT_DEC_SEP +
StringHelper.repeatChar('<', 10);
}
/**
* An equality test which handles the most common cases with the least
* amount of overhead. The superclass implementation is invoked otherwise.
* <p>
* This implementation is consistent with {@link #hashCode()}.
*
* @see com.goldencode.p2j.util.BaseDataType#equals(java.lang.Object)
*/
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o instanceof decimal)
{
decimal that = (decimal) o;
boolean u1 = (this.value == null);
boolean u2 = (that.value == null);
if (u1 && u2)
{
return true;
}
if (u1 || u2)
{
return false;
}
// BigDecimal.equals checks the scale, which may differ, even if the values are the same.
// we need exact comparison for this.
return this.value.compareTo(that.value) == 0;
}
return super.equals(o);
}
/**
* Hash code implementation which is consistent with {@link
* BaseDataType#equals}.
*
* @return Hash code value for this object instance.
*/
public int hashCode()
{
int result = 17;
result = 37 * result + (value == null ? 0 : value.hashCode());
return result;
}
/**
* Returns the value of this instance as a <code>long</code>. Loss of
* precision may occur in this conversion (all digits to the right of
* the decimal point will be truncated).
* <p>
* This method will fail if the instance represents the
* <code>unknown value</code>.
*
* @return The value cast into a <code>long</code>.
*
* @throws NullPointerException
* If this instance represents the <code>unknown value</code>.
*/
public long longValue()
throws NullPointerException
{
if (isUnknown())
{
String errmsg = "Unknown value cannot be converted to long.";
throw new NullPointerException(errmsg);
}
// round to nearest integral value.
BigDecimal d = value.setScale(0, RoundingMode.HALF_UP);
return d.longValue();
}
/**
* Returns the value of this instance as an <code>int</code>. Loss of
* precision may occur in this conversion. All digits to the right of
* the decimal point will be truncated and some large numbers cannot be
* represented in an <code>int</code>.
* <p>
* This method will fail if the instance represents the
* <code>unknown value</code>.
*
* @return The value cast into an <code>int</code>.
*
* @throws NullPointerException
* If this instance represents the <code>unknown value</code>.
* @throws ErrorConditionException
* If the value is too large to fit in an integer.
*/
public int intValue()
throws NullPointerException,
ErrorConditionException
{
if (isUnknown())
{
String errmsg = "Unknown value cannot be converted to int.";
throw new NullPointerException(errmsg);
}
// round to nearest integral value.
long result = value.setScale(0, RoundingMode.HALF_UP).longValue();
// make sure we don't overflow/underflow
if (result > Integer.MAX_VALUE || result < Integer.MIN_VALUE)
{
String errmsg = "Value too large for integer";
ErrorManager.recordOrThrowError(78, errmsg);
return 0;
}
return (int) result;
}
/**
* Returns the value of this instance as an <code>integer</code>. Loss of
* precision may occur in this conversion. All digits to the right of
* the decimal point will be truncated and some large numbers cannot be
* represented in an <code>integer</code>.
* <p>
* This method will maintain the <code>unknown value</code> in the
* returned instance if this instance represents the
* <code>unknown value</code>.
*
* @return The value cast into an integral <code>integer</code>.
*/
public integer integerValue()
{
return (new integer(this));
}
/**
* Returns the value of this instance as a <code>double</code>. Loss
* of precision may occur in this conversion, if the current value cannot
* be represented in a <code>double</code>. This is the same as {@link
* #getValue}. Use {@linkplain #toBigDecimal()} if keeping the precision
* is the concern.
* <p>
* If this instance represents the <code>unknown value</code>, the
* returned <code>double</code> will be set to <code>NaN</code>.
*
* @return The double value of this decimal.
*/
public double doubleValue()
{
if (isUnknown())
{
return Double.NaN;
}
return value.doubleValue();
}
/**
* Returns the value of this instance as a <code>BigDecimal</code>. No loss
* of precision will occur in this conversion.
* <p>
* If this instance represents the <code>unknown value</code>, the
* return value will be <code>null</code>.
*
* @return The <code>BigDecimal</code> instance, see above for details.
*/
public BigDecimal toBigDecimal()
{
return isUnknown() ? null : value;
}
/**
* Increments the value of this instance by 1.
* <p>
* If this instance represents the <code>unknown value</code>, nothing
* is done.
*/
public void increment()
{
if (isUnknown())
{
return;
}
BigDecimal newVal = value.add(BigDecimal.ONE);
checkUndoable(true);
value = newVal;
}
/**
* Decrements the value of this instance by 1.
* <p>
* If this instance represents the <code>unknown value</code>, nothing
* is done.
*/
public void decrement()
{
if (isUnknown())
{
return;
}
BigDecimal newVal = value.subtract(BigDecimal.ONE);
checkUndoable(true);
value = newVal;
}
/**
* Determines if this instance represents the <code>unknown value</code>.
*
* @return <code>true</code> if this instance is set to the
* <code>unknown value</code>.
*/
public boolean isUnknown()
{
return (value == null);
}
/**
* Sets the state of this instance's <code>unknown value</code> state.
* <p>
* <b>Warning: this method is destructive. Any data previously stored
* instance is set to the correct value.</b>
*/
public void setUnknown()
{
if (value != null)
{
checkUndoable(true);
}
value = null;
}
/**
* Creates a new instance of the same type that represents the
* <code>unknown value</code>.
*
* @return An instance that represents the <code>unknown value</code>.
*/
public BaseDataType instantiateUnknown()
{
return new decimal();
}
/**
* Creates a new instance of the same type that represents the
* default initialized value.
*
* @return An instance that represents the default value.
*/
public BaseDataType instantiateDefault()
{
return new decimal("0");
}
/**
* Does the same as standard <code>clone()</code> method but returns an
* instance of <code>BaseDataType</code> and doesn't throw the
* <code>CloneNotSupportedException</code>.
*
* @return A clone of this instance.
*/
public decimal duplicate()
{
return (new decimal(this));
}
@Override
protected boolean isIncompatibleTypesOnConversion(BaseDataType value)
{
// invalid conversion is being thrown even if unknown
if (value instanceof longchar || value instanceof clob || value instanceof blob)
{
incompatibleTypesOnConversion();
}
return super.isIncompatibleTypesOnConversion(value);
}
/**
* Sets the state (data and unknown value) of this instance based on the
* state of the passed instance.
* <p>
* If the value is not of type <code>NumberType</code>, the following automatic type
* conversion will occur:
* <p>
* <ol>
* <li> if the value is a rowid or BinaryData (raw or memptr) it attempts to convert
* the string representation value to a decimal value</li>
* <li> if the value is a character or longchar value it attempts to convert the string
* representation value to a decimal value</li>
* <li> if the value is a non-unknown logical value, it attempts to convert the logical
* value to a decimal value as follow: 0.0 for <b>false</b> and 1.0 for <b>true</b>
* having scale equals to MAX_SCALE</li>
* <li> if the value is a non-unknown date or datetime value the number of days from
* 1/1/4713 B.C. to that date are converted to decimal value.</li>
* <li> if the value is a non-unknown handle or comhandle value, the resource id when
* available is converted to a decimal value, when no resource or resource id the
* value is set to unknown.</li>
* <li> else, it attempts to convert the value's string representation to decimal</li>
* </ol>
* <p>
* This variant is meant to handle the cases of built-in functions and methods in the 4GL
* which have polymorphic return types (e.g. DYNAMIC-FUNCTION()). This should NOT be used for
* non-polymorphic assignments.
*
* @param value
* The instance from which to copy state.
*/
public void assign(BaseDataType value)
{
if (value == null)
{
setUnknown();
return;
}
// the value may be a proxy; make sure to unwrap and work with the real value
value = value.val();
if (value.isUnknown())
{
setUnknown();
}
else if (value instanceof NumberType)
{
// decimal, integer, int64, recid
assign((NumberType) value);
}
else
{
// this path is only used when type morphing is needed
if (value instanceof date && !(value instanceof datetime))
{
// date values only
assign(((date) value).longValue());
}
else if (value instanceof handle)
{
// handle and com-handle
Long id = ((handle) value).getResourceId();
if (id != null)
{
assign(id.longValue());
}
else
{
setUnknown();
}
}
else if (value instanceof logical)
{
assign(((logical) value).booleanValue() ? 1 : 0);
}
else if (value instanceof Text)
{
// character and long character
assign(new decimal(value.toStringMessage()));
}
else if (value instanceof object)
{
assign((object<?>) value);
}
else if (value instanceof jobject)
{
assign((jobject<?>) value);
}
else
{
incompatibleTypesOnConversion();
}
}
}
/**
* Sets the state (data and unknown value) of this instance based on the
* state of the passed instance.
*
* @param value
* The instance from which to copy state.
*/
public void assign(NumberType value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
assign(value.val());
return;
}
if (value instanceof decimal)
{
decimal d = (decimal) value;
// this handles truncation as set by our precision
setValue(d.value);
}
else
{
// must be an integer / int64
if (value.isUnknown())
{
setUnknown();
}
else
{
setValue(value.longValue());
}
}
}
/**
* Sets the state (data/unknown value) of this instance based on the state
* of the passed data. The configuration (precision) of this
* instance is not set. Use {@link #deepAssign} if both state and
* configuration must be set.
* <p>
* This method may result in a loss of precision if the number of
* significant digits to the right of the decimal point is greater than
* that specified by the <code>precision</code> instance member. In this
* case, the excess digits will be dropped using a rounding method (if
* the leftmost lost digit is 0-4 it will round down and 5-9 it will round
* up).
*
* @param value
* The data from which to copy state. <code>NaN</code> will
* force this instance to be the unknown value.
*/
public void assign(double value)
{
assign(new decimal(value));
}
/**
* Sets the state (data) of this instance based on the input data. The
* configuration (precision) of this instance is not set. Use
* {@link #deepAssign} if both state and configuration must be set.
*
* @param value
* The data from which to copy state.
*/
public void assign(int value)
{
assign(new integer(value));
}
/**
* Sets the state (data and unknown value) of this instance based on the
* state of the passed instance.
*
* @param value
* The instance from which to copy state.
*/
public void assign(Undoable value)
{
assign((NumberType) value);
}
/**
* Sets the state (data/unknown value) and configuration (precision) of
* this instance based on the state of the passed instance. No loss of
* precision will occur since the resulting instance will be an identical
* copy of the original value.
*
* @param value
* The instance from which to copy state.
*/
public void deepAssign(NumberType value)
{
if (value.val() instanceof decimal)
{
decimal d = (decimal) value.val();
setValue(d.value);
}
else
{
// integer/int64 path which leverages common code
assign(value.val());
}
}
/**
* Returns the number of digits that will be maintained to the right of
* the decimal point in this instance. By default, this will be 10. All
* assignments to the value of this instance will be rounded/truncated to
* this number of digits. The value can range between 0 and 10
* (inclusive).
*
* @return The precision of this instance.
*/
public int getPrecision()
{
// Note that internally, we refer to this as "scale", to match the
// meaning in BigDecimal, while the API refers to it as "precision" to
// match the legacy terminology.
return scale;
}
/**
* Sets the number of digits that will be maintained to the right of
* the decimal point in this instance. All assignments to the value of
* this instance will be rounded/truncated to this number of digits. The
* value can range between 0 and 10 (inclusive).
* <p>
* This method is used to set the current value in a manner that does not
* schedule an automated reset to the prior value later. Thus, this method
* is suitable for the initial configuration of a new instance. If a
* temporary precision change is needed, use
* {@link #setTemporaryPrecision}.
* <p>
* The precision will become effective on the next value assignment. I.e.
* the value will not change its precision right after this method is called.
*
* @param precision
* The precision for this instance which must range between 0
* and 10 (inclusive).
*/
public void setPrecision(int precision)
{
if (precision < 0 || precision > MAX_SCALE)
{
throw new IllegalArgumentException("Invalid precision " + precision + ".");
}
if (this.scale != precision)
{
checkUndoable(true);
}
// Note that internally, we refer to this as "scale", to match the
// meaning in BigDecimal, while the API refers to it as "precision" to
// match the legacy terminology.
scale = precision;
// the precision will become effective on the next value assignment
}
/**
* Sets the number of digits that will be maintained to the right of
* the decimal point in this instance, for the durations of the current
* scope in the calling program. All assignments to the value of
* this instance within the current scope of the calling program will be
* rounded/truncated to this number of digits. The value can range
* between 0 and 10 (inclusive). When the caller's current scope ends,
* this instance's current precision will be reset to the prior value.
* <p>
* This method should only be used when importing references via the
* {@link SharedVariableManager} as a local instance or a new shared
* instance would not need to be reset, since they would have no prior
* value to which to return. To set the initial precision, please use
* {@link #setPrecision}.
* <p>
* The {@link TransactionManager#registerFinalizable} facility is used to
* obtain the callback necessary to reset the precision at end of the
* caller's scope.
* <p>
* The precision will become effective on the next value assignment. I.e.
* the value will not change its precision right after this method is called.
*
* @param precision
* The precision for this instance which must range between 0
* and 10 (inclusive).
*/
public void setTemporaryPrecision(int precision)
{
// do nothing if the request precision is the same as the current
// precision
if (precision != scale)
{
// remember the old value
int old = scale;
// reset (if there is some error, this will throw an exception)
setPrecision(precision);
// if no error, schedule the reset
PrecisionResetter pr = new PrecisionResetter(old);
TransactionManager.registerFinalizable(pr, false);
}
}
/**
* Compares this instance with the specified instance and returns a -1
* if this instance is less than the specified, 0 if the two instances
* are equal and 1 if this instance is greater than the specified
* instance. This is the implementation of the <code>Comparable</code>
* interface.
* <p>
* The algorithm will fail to give meaningful results in the case where
* one tries to sort against other objects that do not represent compatible
* values.
*
* @param obj
* The instance to compare against.
*
* @return -1, 0 or 1 depending on if this instance is less than,
* equal to or greater (respectively) than the specified
* instance <code>obj</code>.
*/
public int compareTo(Object obj)
{
boolean unknown = isUnknown();
BigDecimal bd = null;
if (obj instanceof decimal)
{
bd = ((decimal) obj).toBigDecimal();
}
else if (obj instanceof BaseDataType)
{
BaseDataType bdt = (BaseDataType) obj;
decimal dec = new decimal(bdt);
/*
* An unknown value should be always converted to a unknown decimal value.
* A non-unknown value should be always converted to a non-unknown decimal value.
* If an unknown value is converted to a non-unknown value the conversion fails.
* If a non-unknown value is converted to an unknown value the conversion fails.
*/
if (dec.isUnknown() != bdt.isUnknown())
{
incompatibleTypesOnConversion();
}
bd = dec.toBigDecimal();
}
else if (obj instanceof BigDecimal)
{
bd = (BigDecimal) obj;
}
else if (obj instanceof Number)
{
bd = new BigDecimal(((Number) obj).doubleValue())
.setScale(MAX_SCALE, RoundingMode.HALF_UP);
}
else if (obj != null)
{
return -1;
}
if (unknown || bd == null)
{
if (value == bd)
{
return 0;
}
else if (unknown)
{
return -1;
}
else
{
return 1;
}
}
return value.compareTo(bd);
}
/**
* Returns the larger of the current or passed-in instance.
*
* @param d
* The instance to compare against.
*
* @return The largest instance.
*/
public decimal maximum(decimal d)
{
return (decimal) super.maximum(d);
}
/**
* Returns the smaller of the current or passed-in instance.
*
* @param d
* The instance to compare against.
*
* @return The smallest instance.
*/
public decimal minimum(decimal d)
{
return (decimal) super.minimum(d);
}
/**
* Return the current value as a Java wrapper.
*
* @return Current value or <code>null</code> if unknown.
*/
public BigDecimal toJavaType()
{
return toBigDecimal();
}
/**
* Creates a string representation of the instance data using the default
* Progress 4GL format of '->>,>>9.99'.
*
* @return The formatted string.
*/
public String toString()
{
return toString(null);
}
/**
* Creates a string representation of the instance data in a form that is
* compatible with the <code>MESSAGE</code> language statement. If the
* instance represents the <code>unknown value</code>, a '?' will be
* returned.
*
* @return The 'message' formatted string.
*/
public String toStringMessage()
{
return toStringExport();
}
/**
* Creates a string representation of the instance data using the 'export'
* format.
*
* @return The 'export' formatted string.
*/
public String toStringExport()
{
if (isUnknown())
{
return "?";
}
// quick out (actually this is not just performance but is needed
// because the processing below will generate an empty string for
// this case)
if (value.compareTo(ZERO) == 0)
{
return "0";
}
String result = toString(getExportFormat(this));
StringBuilder sb = new StringBuilder(result.trim());
// some cleanups to match export format
if (sb.charAt(sb.length() - 1) == getDecimalSeparator())
{
// get rid of trailing '.' chars (with no digits to right)
result = sb.substring(0, (sb.length() - 1));
}
else if (sb.indexOf("0") == 0)
{
// get rid of leading '0' char (if there are no digits to left)
// in a positive number
result = sb.substring(1);
}
else if (sb.indexOf("-0") == 0 || sb.indexOf("- ") == 0)
{
// get rid of leading '0' or ' ' char (if there are no digits to
// left) in a negative number
result = sb.deleteCharAt(1).toString();
}
else
{
// if nothing else, make sure our trimmed result is what is returned
result = sb.toString();
}
return result;
}
/**
* Creates a string representation of the instance data using the user
* specified format string (Progress 4GL compatible) or if no such
* string is provided, the default format of '->>,>>9.99'.
*
* @param fmt
* The Progress 4GL format string or <code>null</code> if the
* default format is to be used.
*
* @return The formatted string.
*/
public String toString(String fmt)
{
return toString(fmt, true);
}
/**
* Creates a string representation of the instance data using the user
* specified format string (Progress 4GL compatible) or if no such
* string is provided, the default format of '->>,>>9.99'.
*
* @param fmt
* The Progress 4GL format string or <code>null</code> if the
* default format is to be used.
* @param rightAlign
* Pad on the left to the full size of the format string if the
* format has '>' characters that match the positions of
* leading zeroes.
*
* @return The formatted string.
*/
public String toString(String fmt, boolean rightAlign)
{
return toString(fmt, rightAlign, false);
}
/**
* Creates a string representation of the instance data using the user
* specified format string (Progress 4GL compatible) or if no such
* string is provided, the default format of '->>,>>9.99'.
*
* @param fmt
* The Progress 4GL format string or <code>null</code> if the
* default format is to be used.
* @param rightAlign
* Pad on the left to the full size of the format string if the
* format has '>' characters that match the positions of
* leading zeroes.
* @param forceNegative
* Flag indicating the forcing including the negative sign value
* in the case when the real value is 0.
*
* @return The formatted string.
*/
public String toString(String fmt, boolean rightAlign, boolean forceNegative)
{
if (isUnknown())
{
return "?";
}
if (fmt == null)
{
fmt = getDefaultFormat(this);
}
char groupSep = getGroupSeparator();
char decSep = getDecimalSeparator();
String full = getDecimalFormat().format(value.abs());
int pos = full.indexOf(decSep);
String left;
String right;
if (pos < 0)
{
// no decimal point
left = full;
right = "";
}
else
{
// standard case
left = full.substring(0, pos);
right = full.substring(pos + 1);
}
boolean negative = forceNegative ? true : (value.signum() < 0);
String result = toString(left, right, negative, fmt, rightAlign);
// possible missing sign or left side overflow
if (result.indexOf('?') == 0)
{
// short-circuit processing and return immediately
if (StringHelper.repeatChar('?', fmt.length()).equals(result))
{
return result;
}
}
int decPt = result.indexOf(decSep);
int expected = right.length();
int size = 0;
if (decPt >= 0)
{
int len = result.length();
if (decPt < len - 1)
{
int i = decPt + 1;
char c = result.charAt(i);
// calculate the total number of chars that was output on the
// right side of the decimal point
while ((c >='0' && c <= '9') || c == groupSep)
{
// only count digits
if (c != groupSep)
{
size++;
}
i++;
if (i == len)
break;
c = result.charAt(i);
}
}
else
{
expected = 0;
}
}
// unfortunately, decimals MAY HAVE an ADDITIONAL rounding step based
// on whether there are unprocessed digits left on the right side
if (size < expected)
{
BigDecimal bd = this.value.setScale(size, RoundingMode.HALF_UP);
negative = forceNegative ? true : (bd.signum() < 0);
// redo everything again, with the value already rounded to the
// right place
full = getDecimalFormat().format(bd.abs());
pos = full.indexOf(decSep);
if (pos < 0)
{
// no decimal point --> this can happen in a case like 9999.995
// and a format string of "$>,>>9.99", where expected will be 3
// and size will be 2, thus the newVal is 10000.00, full is
// "10000" and pos is thus -1
left = full;
right = "";
}
else
{
// standard case
left = full.substring(0, pos);
right = full.substring(pos + 1);
}
result = toString(left, right, negative, fmt, rightAlign);
}
return result;
}
/**
* Obtain the length (in bytes) of this BDT will use when serialized.
*
* @return the length of this BDT will use when serialized.
*/
@Override
public int getSize()
{
if (isUnknown() || value.compareTo(new BigDecimal(0)) == 0)
{
return 1;
}
String asStr = value.toPlainString();
int significant = asStr.length();
if (asStr.indexOf('.') >= 0)
{
// count trailing 0-s
while (asStr.charAt(significant - 1) == '0')
{
-- significant;
}
-- significant; //count out the '.'
}
if (asStr.charAt(0) == '-')
{
-- significant; //count out the '-'
if (asStr.charAt(1) == '0')
{
-- significant; //count out the heading '0'
}
}
else if (asStr.charAt(0) == '0')
{
-- significant; //count out the heading '0'
}
return 2 + (significant + 1) / 2;
}
/**
* Replacement for the default object reading method. The latest
* state is read from the input source.
*
* @param in
* The input source from which fields will be restored.
*
* @throws IOException
* In case of I/O errors.
* @throws ClassNotFoundException
* If payload can't be instantiated.
*/
public void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
boolean unknown = in.readBoolean();
scale = in.readInt();
if (!unknown)
{
value = (BigDecimal) in.readObject();
}
}
/**
* Replacement for the default object writing method. The latest
* state is written to the output destination.
*
* @param out
* The output destination to which fields will be saved.
*
* @throws IOException
* In case of I/O errors.
*/
public void writeExternal(ObjectOutput out)
throws IOException
{
boolean unknown = (isUnknown());
out.writeBoolean(unknown);
out.writeInt(scale);
if (!unknown)
{
out.writeObject(value);
}
}
/**
* Worker method for setBits, this copies the bit representation of an
* integer to a location with a given number of bits within the current integer variable.
*
* @param data
* The bit representation of an integer to be copied.
* @param pos
* The 1-based index position to set, must be > 0.
* @param len
* The number of bytes to take into consideration when performing the operation.
*/
public void setBitsWorker(double data, double pos, double len)
{
// TODO Implement actual logic for decimal
// throw error: Incompatible data types in expression or assignment(223)
}
/**
* Worker method for getBits, this returns the number of bits from the
* given position as an <code>integer</code>.
*
* @param pos
* The 1-based index position to set, must be > 0.
* @param len
* The number of bits to take into consideration when performing the operation.
*
* @return The number of bits from the given position as an <code>integer</code>.
*/
public integer getBitsWorker(double pos, double len)
{
// TODO Implement actual logic for decimal
// throw error: Incompatible data types in expression or assignment(223)
return new integer();
}
/**
* Test if the given input would cause an overflow error in the 4GL and raise that
* error if needed.
*
* @param bd
* The input to test. It should already have been scaled to the target level.
* @param normal
* <code>true</code> if all trailing zeroes have been removed already.
* @param raise
* <code>true</code> if the error should be raised, <code>false</code> to
* bypass error generation and simply just return the calculated result.
*
* @return <code>true</code> if an overflow would occur (the return in this case won't
* be seen unless silent error mode is on). <code>false</code> otherwise.
*/
public boolean isOverflow(BigDecimal bd, boolean normal, boolean raise)
{
boolean fail = false;
int total = bd.precision();
int fract = bd.scale();
int left = total - fract;
// negative scale case
if (fract < 0)
{
// there are no digits to the right of the decimal point but there are some
// extra digits on the left (minus a minus is plus) => unscaled * 10 ^ -scale
if (left > 50)
{
fail = true;
}
}
else
{
// check for overflow
if (left > 50 || (left > 40 && (total > 50)))
{
// if truncation occurred, it may have left trailing zeros which may have
// caused us to detect overflow unnecessarily, use recursion to solve this;
// by doing it this more complex way, we avoid the use of stripTrailingZeros
// except in very rare cases; this code is in a performance sensitive path
// so optimization is important (otherwise we could have just universally
// stripped the trailing zeroes on entry to this method)
fail = (normal || isOverflow(bd.stripTrailingZeros(), true, raise));
}
}
if (fail && raise)
{
ErrorManager.recordOrThrowError(536, "Decimal number is too large");
}
return fail;
}
/**
* Returns the value of this instance as a <code>double</code>. Loss
* of precision may occur in this conversion, if the current value cannot
* be represented in a <code>double</code>. This is the same as {@link #doubleValue}.
* <p>
* If this instance represents the <code>unknown value</code>, the
* returned <code>double</code> will be set to <code>NaN</code>.
*
* @return The value cast into a <code>double</code>.
*/
double getValue()
{
return doubleValue();
}
/**
* Get the default initialization for an extent variable of this type.
*
* @return See above.
*/
@Override
protected BaseDataType instantiateDefaultExtent()
{
return new decimal(0);
}
/**
* Direct-assign the specified non-fractional number (string representation), using the given
* scale. After a {@link BigDecimal} instance is created, the value is divided by
* {@code 10^scale}. This instance's {@link #scale} value will not be changed.
*
* @param num
* The non-fractional number.
* @param scale
* The fractional scale.
*/
void setValue(String num, int scale)
{
BigDecimal bd = new BigDecimal(num);
bd.setScale(scale);
bd = bd.divide(new BigDecimal("1e" + scale));
this.value = bd;
}
/**
* Set the value of this instance from the given numeric value. A loss
* of precision will occur in this conversion if the passed value has
* more significant digits than this instance's current scale. The
* resulting value will be rounded to the proper number of digits.
* <p>
* If the passed-in value is <code>null</code>, the instance will
* represent the <code>unknown value</code>. If this instance previously
* represented the <code>unknown value</code> and <code>null</code> is not
* passed, the <code>unknown value</code> state will be
* <code>false</code>.
*
* @param value
* The value to be represented in this instance.
*/
private void setValue(BigDecimal value)
{
if (value == null)
{
if (this.value != null)
{
checkUndoable(true);
}
this.value = null;
return;
}
if (isAssignDirect())
{
this.value = value;
return;
}
BigDecimal bd = null;
if (scale == value.scale())
{
// it is safe to assign reference because BigDecimal is immutable
bd = value;
}
else
{
// preserve original scale instead of picking up new one
bd = value.setScale(scale, RoundingMode.HALF_UP);
}
if (!isOverflow(bd, false, true))
{
if (this.value == null || this.value.compareTo(bd) != 0)
{
checkUndoable(true);
}
this.value = bd;
}
}
/**
* Set the value of this instance from a Progress-formatted string. A
* loss of precision will occur in this conversion if the passed value has
* more significant digits than the setting of the <code>scale</code>
* instance member of this object. The resulting value will be rounded
* to the proper number of digits.
* <p>
* If the passed-in value is <code>null</code>, the instance will
* represent the <code>unknown value</code>. If this instance previously
* represented the <code>unknown value</code> and <code>null</code> is not
* passed, the <code>unknown value</code> state will be
* <code>false</code>.
*
* @param value
* The value to be represented in this instance.
*/
private void setValue(String value)
{
if (value == null)
{
if (this.value != null)
{
checkUndoable(true);
}
this.value = null;
return;
}
// this will also check for overflows, so it is safe to assign the result below
String dstr = parseDecimal(value, false);
if (dstr != null)
{
BigDecimal newVal = new BigDecimal(dstr).setScale(scale, RoundingMode.HALF_UP);
if (this.value == null || this.value.compareTo(newVal) != 0)
{
checkUndoable(true);
}
this.value = newVal;
}
}
/**
* Set the value of this instance from 64-bit signed integer. No loss of precision will occur. This
* method directly avoids transforming the value into a Java double, which might have caused a loss of
* precision since not all possible signed 64-bit integer values can be safely represented in a 64-bit
* double.
*
* @param value
* The value to be represented in this instance.
*/
private void setValue(long value)
{
BigDecimal newVal = new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP);
if (this.value == null || this.value.compareTo(newVal) != 0)
{
checkUndoable(true);
}
this.value = newVal;
}
/**
* Set the value of this instance as a <code>double</code>. A loss
* of precision will occur in this conversion if the passed value has
* more significant digits than the setting of the <code>precision</code>
* instance member of this object. The resulting value will be rounded
* to the proper number of digits.
* <p>
* If the passed-in value is <code>NaN</code> or infinity (positive or negative), the
* instance will represent 0, which is how the 4GL deals with these values when reading
* them directly from memory (e.g. when passed back via a native API call). Actually
* with infinity, the 4GL goes into an infinite loop and needs to be hard-killed, but
* this code won't duplicate that "feature".
*
* @param value
* The value to be represented in this instance.
*/
private void setValue(double value)
{
// handle boundary conditions (BigDecimal won't like these as inputs)
if (Double.isNaN(value) || Double.isInfinite(value))
{
value = 0;
}
BigDecimal bd = new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP);
if (!isOverflow(bd, false, true))
{
if (this.value == null || this.value.compareTo(bd) != 0)
{
checkUndoable(true);
}
this.value = bd;
}
}
/**
* Helper to reset the associated instance's precision to a prior value
* based on a notification that a particular scope has exited. This
* scope notification support is provided by
* {@link TransactionManager#registerFinalizable} and the {@link
* Finalizable} interface.
*/
private class PrecisionResetter
implements Finalizable
{
/** The precision to which the containing instance will be reset. */
int oldPrecision = 0;
/**
* Construct an instance which can reset the containing instance's
* precision when a notification is received. See
* {@link decimal#setTemporaryPrecision}.
*
* @param oldPrecision
* The precision to which the containing instance must be
* reset.
*/
PrecisionResetter(int oldPrecision)
{
this.oldPrecision = oldPrecision;
}
/**
* Processes the end of scope notification and resets the associated
* instance's precision. This notification is provided by the
* {@link TransactionManager}.
*/
public void finished()
{
setPrecision(oldPrecision);
}
/**
* Provides a notification that the external program scope in which the object is registered is
* is being deleted and the object's reference will be lost after this method is called.
* <p>
* This is a no-op in this case.
*/
@Override
public void deleted()
{
// no-op
}
/**
* Processes the iterate notification provided by the {@link
* TransactionManager}. This implementation does nothing.
*/
public void iterate()
{
}
/**
* Processes the retry notification provided by the {@link
* TransactionManager}. This implementation does nothing.
*/
public void retry()
{
}
}
/**
* Stores global data relating to the state of the current context.
*/
private static class WorkArea
{
/**
* Stores the context-specific default format string (which honors
* the user's defined group and decimal separators). Initialization
* is delegated to the implementation class.
*/
private String defFmt = null;
/** The default decimal formatter. */
private DecimalFormat decimalFmt = null;
}
/**
* Simple container that stores and returns a context-local instance of
* the global work area.
*/
private static class ContextContainer
extends ContextLocal<WorkArea>
{
/**
* Initializes the work area, the first time it is requested within a
* new context.
*
* @return The newly instantiated work area.
*/
protected WorkArea initialValue()
{
return new WorkArea();
}
}
/**
* Simple command line harness to test the toString() method.
*
* @param args
* 1. numeric value
* 2. format string
*/
public static void main(String[] args)
{
try
{
decimal d = new decimal(args[0]);
System.out.println("Formatted = '" + d.toString(args[1]) +
"', Export = " + d.toStringExport());
}
catch (Exception exc)
{
LOG.severe("", exc);
}
}
}