BigDecimal e MathContext

Fala pessoal!
Tenho um código simples pro BU(BrazilUtils) que tá dando uma pequena dor de cabeça…
Tenho uma classe básica de Unidades:

[code]
import java.math.*;

public class Unit {

String name;

BigDecimal valueOnBaseUnit;

public Unit(String name, BigDecimal valueOnBaseUnit) {
	this.name = name;
	this.valueOnBaseUnit = valueOnBaseUnit;
}

public String getName() {
	return name;
}

public void setName(String name) {
	this.name = name;
}

public BigDecimal getValueOnBaseUnit() {
	return valueOnBaseUnit;
}

public void setValueOnBaseUnit(BigDecimal valueOnBaseUnit) {
	this.valueOnBaseUnit = valueOnBaseUnit;
}

@Override
public String toString() {
	return "[name=" + name + ",value=" + valueOnBaseUnit + "]";
}

}[/code]
2 interfaces, sendo uma com o método UnitWithValue convertTo(Unit unit); e a outra com as constantes que eu quero:

public interface AreaMetrics { //A base é Metros Quadrados /*Square Metric is the unit base */ Unit M2 = new Unit("m²", new BigDecimal(String.valueOf(1))); Unit ACRE=new Unit( "acre",new BigDecimal(4046.8564224 )); Unit ARES=new Unit("ares",new BigDecimal(String.valueOf(100))); //blablabla Unit QUILOMETRO_QUADRADO=new Unit("quilometro_quadrado" ,new BigDecimal(String.valueOf(1000000))); Unit BASE=M2; }
E a classe principal de Área:

[code]
import java.math.BigDecimal;
//import org.brazilutils.metrics.*;

public class Area implements UnitWithValue,AreaMetrics {

private static final long serialVersionUID = 210046788217078583L;
private Unit unit;
private BigDecimal value;
private BigDecimal base;


public Area(BigDecimal value,Unit unit,int roudingMode,int scale){
	this.base = value.multiply(unit.getValueOnBaseUnit());//,roudingMode,scale);
	this.value = value;
	this.unit = unit;
}

public Area(BigDecimal value,Unit unit){
	this.base = value.multiply(unit.getValueOnBaseUnit());
	this.value = value;
	this.unit = unit;
}	

public String toString() {
	return "[valor= " + value + ",unidade= " + unit + ",valorNaBase= " + base + "]";
}

/*public UnitWithValue convertTo(Unit unit,int scale,int roundingmode) {
	return new Area(base.divide(unit.getValueOnBaseUnit(),scale,roundingmode),unit);
}**/

public UnitWithValue convertTo(Unit unit){
	return new Area(base.divide(unit.getValueOnBaseUnit()),unit);
}

public Unit getUnit(){
	return unit;
}

public void setUnit(Unit u) {
	this.unit = u;
	this.value = base.divide(unit.getValueOnBaseUnit());//, int round);
}

public static void main(String[] args) {
	Area area1 = new Area(new BigDecimal(10000), Area.M2);
	System.out.println(area1);
	System.out.println(area1.convertTo(Area.HECTARE));
	System.out.println(area1.convertTo(Area.ACRE));
}

}[/code]
Meu problema:
Eu não consigo dividir corretamente(recebo uma AritmeticException) porque eu não consigo precisar o resultado de forma decimal.Não posso usar MathContext(Aí seria mole…) porquê o código TEM que ser compatível com o Java 1.4.Alguém tem uma idéia de como eu poderia evitar isso(de preferência de forma elegante…)

Iron, ao invés de

public UnitWithValue convertTo(Unit unit) { return new Area(base.divide(unit.getValueOnBaseUnit()),unit); }
use

public UnitWithValue convertTo(Unit unit) { return new Area(base.divide(unit.getValueOnBaseUnit(), 2, RoundingMode.HALF_EVEN), unit); }

Aqui, funcionou.

Testa aí.

Só que assim é “sopa com mel” Rafael! :smiley:
Vc está decidindo pelo usuário ao usar RoundingMode.HALF_EVEN,
e se ele quiser ROUND_HALF_DOWN, sacou?
Talvez seja melhor permitir que o usuario esoclha o roundmode e a escala via construtor…

Tranqüilo, Iron! A classe é sua! hehe.

Se rodou, agora é só definir os parâmetro que você quer no método e passar pro método divide. Certo?

Sugestão: incializa as variáveis BigDecimals com String, ao invés de double.

Olha o porquê: http://rfiume.blogspot.com/2006/12/bigdecimal-com-string-para-melhor.html

E me fala se deu tudo certo aí, beleza?

T+!

Sem essa, todas as classes são nossas!!!

Certo?

Não.Pq o usuário não consegue passar esse parâmetro até esse divide.Se for para usar o Rounding Mode, deveremos deixar o usuário escolher o dele, e talvez a escala também.Mas daí eu pergunto, como fazer isso de uma forma elegante?Reescrevemos o multiply?
Realmente o MathContext quebra um galhão…

[quote]incializa as variáveis BigDecimals com String, ao invés de double.
[/quote]
Eu tô usando double justamente para testes, pq eu sei que perde a precisão…E eu acho que acabo deixando tudo String até pq diminui a necessidade dos testes…

Você não quer mudar a assinatura do método convertTo, certo?

O que acha de criar duas propriedades para a sua classe, scale e roundingMode, e inicializá-las com valores padrão: 2 e RoundingMode.HALF_EVEN.

Nos casos em que o usuário da classe precisar de outra escala ou modo de arredondamento, ele ajusta através dos set’s.

Humm, não saquei bem… posta um exemplo!

Mas eu passando esses valores padrão?
Temos que pensar que podem rolar números com 30 casas decimais, o cara (o usuário) pode querer decidir isso…

Ele muda a escala partir dos modificadores:

public void setScale(int num);

A mesma coisa para RoundingMode.

Acredito que escala == 2, e RoundingMode == HALF_EVEN, sejam os valores mais comuns para operações com unidade de medidas, então esses valores podem ser estabelecidos como padrão.

Quando o cara precisar de escala 30, ele usa:

setScale(30)

O usuário tem como ajustar essas propriedades a vontade, mas só vai ter que lidar com elas caso precise de valores diferentes do padrão. O método convertTo também vai ficar com a assinatura que você quer:

converTo(Unit unit)

Mas aí tá rolando uma suposição nossa… talvez o ideal, seria ver o código de MathContext e simularmos a constante DECIMAL128, o que garante 34 casas de precisão.Dá uma olhada:
http://java.sun.com/j2se/1.5.0/docs/api/java/math/MathContext.html

Essa classe realmente é o que precisamos:
[b]
The base-independent settings are:

  1. precision: the number of digits to be used for an operation; results are rounded to this precision
  2. roundingMode: a RoundingMode object which specifies the algorithm to be used for rounding. [/b]

É suposição! 8)

E como toda suposição, pode ser que não seja o melhor, por isso tem que ser validada pelos usuários da classe. Conforme o feedback deles, isso pode ser mudado no futuro.

Se entendi bem, você quer um Ctrl+C / Ctrl+V na classe MathContext. Já fiz algo assim uma vez e não gostei por vários motivos. Entre eles, gerou confusão pra mim mesmo. Imagine então numa API pública!!

Não resolvemos ficar no Java 1.4? :roll: É o preço a pagar.

[Editado] Detalhe: o RoundingMode do DECIMAL128 é HALF_EVEN. [/Editado]

Não… é rewrite mesmo, pegando o que nos for útil…

Não resta dúvidas quanto a isso…

Rafael, vou te falar de algumas modificações que eu fiz:
Parece que RoundingMode.HALF_EVEN é java 1.5, logo tive que usar BigDecimal.ROUND_HALF_EVEN quando necessário.

Penso seriamente em deixa o convertTo ao gosto do usuário, pois assim ele escolhe:
public UnitWithValue convertTo(Unit unit,int scale,int roundingmode) {
return new Area(base.divide(unit.getValueOnBaseUnit(),scale,roundingmode),unit);
}

Não se preocupe que eu uso o String.valueOf para manter a retrocompatibilidade nas interfaces:

public interface AreaMetrics { //A base é Metros Quadrados /*Square Metric is the unit base */ Unit M2 = new Unit("m²", new BigDecimal(String.valueOf(1))); Unit ACRE=new Unit( "acre",new BigDecimal(String.valueOf(4046.8564224) )); Unit ARES=new Unit("ares",new BigDecimal(String.valueOf(100)));

Vc preferiria a manutenção do convertTo isolado?Exemplo com as 2 formas:

[code]
import java.math.BigDecimal;
//import org.brazilutils.metrics.*;

public class Area implements UnitWithValue,AreaMetrics {

private static final long serialVersionUID = 210046788217078583L;
private Unit unit;
private BigDecimal value;
private BigDecimal base;


public Area(BigDecimal value,Unit unit,int roudingMode,int scale){
	this.base = value.multiply(unit.getValueOnBaseUnit());//,roudingMode,scale);
	this.value = value;
	this.unit = unit;
}

public Area(BigDecimal value,Unit unit){
	this.base = value.multiply(unit.getValueOnBaseUnit());
	this.value = value;
	this.unit = unit;
}	

public String toString() {
	return "[value= " + value + ",unit= " + unit + ",base= " + base + "]";
}

public UnitWithValue convertTo(Unit unit,int scale,int roundingmode) {
	return new Area(base.divide(unit.getValueOnBaseUnit(),scale,roundingmode),unit);
}

public UnitWithValue convertTo(Unit unit){
    return new Area(base.divide(unit.getValueOnBaseUnit(), 34, BigDecimal.ROUND_HALF_EVEN), unit);
}

public Unit getUnit(){
	return unit;
}

public void setUnit(Unit u) {
	this.unit = u;
	this.value = base.divide(unit.getValueOnBaseUnit());//, int round);
}

public static void main(String[] args) {
	Area area1 = new Area(new BigDecimal(10000), Area.M2);
	System.out.println(area1);
	System.out.println(area1.convertTo(Area.HECTARE,34, BigDecimal.ROUND_HALF_EVEN));
	System.out.println(area1.convertTo(Area.ACRE));
}	

}[/code]

Por que não as duas formas, com sobrecarga:

public UnitWithValue convertTo(Unit unit)
public UnitWithValue convertTo(Unit unit,int scale,int roundingmode)

No primeiro caso, o método tem que ter uma documentação precisa para que o usuário saiba exatamente qual o resultado da operação.

public UnitWithValue convertTo(Unit unit){ return new Area(base.divide(unit.getValueOnBaseUnit(), 34, BigDecimal.ROUND_HALF_EVEN), unit); }
Resolveu esquecer a re-implementação do MathContext?

[quote=Ironlynx]
Não se preocupe que eu uso o String.valueOf para manter a retrocompatibilidade nas interfaces[/quote]
Não entendi Iron. Pra que String.valueOf ? O que vai influenciar na retrocompatibilidade?

Noop, é que com o convertTo definido pelo usuário, seria desnecessário.
Olha como é MathContext:

import java.io.Serializable;

/**
 * Immutable objects describing settings such as rounding mode and digit
 * precision for numerical operations such as those in the BigDecimal class.
 * @author Anthony Balkissoon abalkiss at redhat dot com
 *
 */
public final class MathContext implements Serializable
{
  /** A MathContext for unlimited precision arithmetic * */
  public static final MathContext UNLIMITED = 
    new MathContext(0, RoundingMode.HALF_UP);
  
  /**
   * A MathContext for the IEEE 754R Decimal32 format - 7 digit preicision and
   * HALF_EVEN rounding.
   */
  public static final MathContext DECIMAL32 = 
    new MathContext(7, RoundingMode.HALF_EVEN);
  
  /**
   * A MathContext for the IEEE 754R Decimal64 format - 16 digit preicision and
   * HALF_EVEN rounding.
   */
  public static final MathContext DECIMAL64 = 
    new MathContext(16, RoundingMode.HALF_EVEN);
  
  /**
   * A MathContext for the IEEE 754R Decimal128 format - 34 digit preicision and
   * HALF_EVEN rounding.
   */
  public static final MathContext DECIMAL128 = 
    new MathContext(34, RoundingMode.HALF_EVEN);
  
  /**
   * This is the serialVersionUID reported here:
   * java.sun.com/j2se/1.5.0/docs/api/serialized-form.html#java.math.MathContext
   */
  private static final long serialVersionUID = 5579720004786848255L;
  
  private int precision;
  
  private RoundingMode roundMode;
  
  /**
   * Constructs a new MathContext with the specified precision and with HALF_UP
   * rounding.
   * @param setPrecision the precision for the new MathContext
   * 
   * @throws IllegalArgumentException if precision is &lt 0.
   */
  public MathContext(int setPrecision)
  {
    this(setPrecision, RoundingMode.HALF_UP);
  }
  
  /**
   * Constructs a new MathContext with the specified precision and rounding
   * mode.
   * @param setPrecision the precision
   * @param setRoundingMode the rounding mode
   * 
   * @throws IllegalArgumentException if precision is &lt 0.
   */
  public MathContext(int setPrecision, RoundingMode setRoundingMode)
  {
    if (setPrecision &lt 0)
      throw new IllegalArgumentException("Precision cannot be less than zero.");
    precision = setPrecision;
    roundMode = setRoundingMode;
  }
  
  /**
   * Constructs a MathContext from a String that has the same form as one
   * produced by the toString() method.
   * @param val
   * 
   * @throws IllegalArgumentException if the String is not in the correct
   * format or if the precision specified is &lt 0.
   */
  public MathContext(String val)
  {
    try
    {
      int roundingModeIndex = val.indexOf("roundingMode", 10);
      precision = Integer.parseInt(val.substring(10, roundingModeIndex - 1));
      roundMode = RoundingMode.valueOf(val.substring(roundingModeIndex + 13));
    }
    catch (NumberFormatException nfe)
    {
      throw new IllegalArgumentException("String not in correct format");
    }
    catch (IllegalArgumentException iae)
    {
      throw new IllegalArgumentException("String not in correct format");
    }
    if (precision &lt 0)
      throw new IllegalArgumentException("Precision cannot be less than 0.");
  }
  
  /**
   * Returns true if x is a MathContext and has the same precision setting
   * and rounding mode as this MathContext.
   * 
   * @return true if the above conditions hold
   */
  public boolean equals(Object x)
  {
    if (!(x instanceof MathContext))
      return false;
    MathContext mc = (MathContext)x;
    return mc.precision == this.precision
           && mc.roundMode.equals(this.roundMode);
  }
  
  /**
   * Returns the precision setting.
   * @return the precision setting.
   */
  public int getPrecision()
  {
    return precision;
  }
  
  /**
   * Returns the rounding mode setting.  This will be one of 
   * RoundingMode.CEILING, RoundingMode.DOWN, RoundingMode.FLOOR, 
   * RoundingMode.HALF_DOWN, RoundingMode.HALF_EVEN, RoundingMode.HALF_UP, 
   * RoundingMode.UNNECESSARY, or RoundingMode.UP.
   * @return the rounding mode setting.
   */
  public RoundingMode getRoundingMode()
  {
    return roundMode;
  }
  
  /**
   * Returns "precision=p roundingMode=MODE" where p is an int giving the 
   * precision and MODE is UP, DOWN, HALF_UP, HALF_DOWN, HALF_EVEN, CEILING,
   * FLOOR, or UNNECESSARY corresponding to rounding modes.
   * 
   * @return a String describing this MathContext
   */
  public String toString()
  {
    return "precision="+precision+" roundingMode="+roundMode;
  }
  
  /**
   * Returns the hashcode for this MathContext.
   * @return the hashcode for this MathContext.
   */
  public int hashCode()
  {
    return precision ^ roundMode.hashCode();
  }
}

Se for usá-la, chamo de BigDecimalContext???Eu vou ter que substituir a Enumeração (o RoundingMode), e as constantes por BigDecimal.RoundingQualquerCoisa

Eu pensei que já havia lhe dito… é que no java 1.4, BigDecimal tinha apenas 4 construtores(para String,double, e BigInteger com e sem escala), e no Tiger(1.5) são 14!!!Vc pode tacar um valor BigDecimal que não dará erro de compilação.Mas no 1.4, o construtor não será reconhecido… por isso o String.valueOf…

E depois, [quote=RafaelRio]
Se rodou, agora é só definir os parâmetro que você quer no método e passar pro método divide. [/quote]
Por mim, deixa assim para a versão 0.1, daí não seria preciso “re-implementar” o MathContext. public UnitWithValue convertTo(Unit unit,int scale,int roundingmode)

[quote=Ironlynx]
Olha como é MathContext:[/quote]
Mas isso eu vejo no código-fonte. Você podia ter mandado o seu MathContext.

[quote=Ironlynx]
Se for usá-la, chamo de BigDecimalContext???[/quote]
Que tal BrazilUtilsMathContext? :lol:

[quote=Ironlynx]
Eu pensei que já havia lhe dito… é que no java 1.4…[/quote]
Não cheguei a lidar com Java 1.4, já fui direto pro 5. No dia-a-dia também não uso a versão 1.4. A última coisa que você deve esperar que eu saiba é esse tipo de detalhe. Aliás, agora já estou no Java 6! :smiley:

[quote=Ironlynx]
Noop[/quote]
What hell is Noop? Será que eu não posso mais me considerar um nerds de carteirinha por não saber o que é isso?

Por mim, deixa assim para a versão 0.1, daí não seria preciso "re-implementar" o MathContext. 

Blz então!A única diferença era encher de constantes BigDecimal.Rounding…

NOP ou NOOP (abreviatura de No OPeration) que é uma instrução em assembly, mas na verdade, pode ser usado apenas para dizer que “Não é assim”, ou não funciona!Sacou?

O garoto tá moderno… :smiley:
Mas eu tb jah tô no Mustang, mas tá longe de eu sacar a maioria das features dele…