Ratio

Objetivo

Permitir manipular frações minimizando as operações de divisão.

Propósito

A operação de divisão é uma das mais complexas no mundo da computação porque leva facilmente a uma perda de exatidão nos cálculos. Usando valores primitivos de ponto flutuante como double e float isso se torna mais óbvio, mas mesmo utilizando BigDecimal esse problema se coloca ao termos que escolher o modo de arredondamento a cada divisão.

O padrão Ratio (Razão, Proporção) minimiza o problema ao adiar ao máximo a necessidade de executar a divisão, controlando o valor do numerador e do denominador separadamente e usando as regras aritmétidas para frações.

Este padrão também é conhecido como Rational já que simula um número racional.

Implementação

Para este exemplo de implementação usaremos a classe BigInteger como base para mantermos os valores do numerador e do denominador sem nos preocuparmos com overflow. Poderiamos usar longs ou ints, mas teriamos que permanentemente fazer testes de overflow. A classe BigInteger já faz isso para nós.

public class Ratio {

	public static Ratio valueOf(String value){
		return valueOf(new BigDecimal(value));
	}

	public static Ratio valueOf(BigDecimal value){
		int scale = value.scale();

		BigDecimal numerator = value.scaleByPowerOfTen(scale);

		BigDecimal denominator = BigDecimal.TEN.scaleByPowerOfTen(scale - 1);

		return reduce(numerator.toBigInteger(), denominator.toBigInteger());
	}

	public static Ratio valueOf(String numerator, String denominator){
		return new Ratio(new BigInteger(numerator), new BigInteger(denominator));
	}

	public static Ratio valueOf(double value){

		if (Double.isInfinite(value)) {
			throw new IllegalArgumentException("Value must be finite");
		}
		if (Double.isNaN(value)) {
			throw new IllegalArgumentException("Value must be a number. NaN is not acceptable.");
		}

		return valueOf(Double.toString(value));

	}

	public static Ratio valueOf(long numerator){
		return new Ratio(BigInteger.valueOf(numerator), BigInteger.ONE);
	}

	public static Ratio valueOf(long numerator, long denominator){
		return reduce(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
	}


	private static Ratio reduce(BigInteger numerator, BigInteger denominator) {

		if (denominator.equals(BigInteger.ZERO)) {
			throw new IllegalArgumentException("Denominator cannot be zero");
		}

		if (denominator.compareTo(BigInteger.ZERO) < 0) {
			numerator = numerator.negate();
			denominator = denominator.negate();
		}

		BigInteger gcd = numerator.gcd(denominator);

		return new Ratio(numerator.divide(gcd), denominator.divide(gcd));
	}

	private final BigInteger numerator;
	private final BigInteger denominator;

	private Ratio(BigInteger numerador, BigInteger denominador){
		this.numerator = numerador;
		this.denominator = denominador;
	}

	public Ratio plus(Ratio other){
		// a soma de a/b + c/d é igual a (ad+cb)/bd

		BigInteger ad = this.numerator.multiply(other.denominator);
		BigInteger cd = this.denominator.multiply(other.numerator);

		BigInteger bd = this.denominator.multiply(other.denominator);

		return new Ratio(ad.add(cd), bd); // não ha divisão
	}

	public Ratio subtract(Ratio other){
		return this.plus(other.negate());
	}

	public Ratio negate(){
		return new Ratio(this.numerator.negate(), this.denominator);
	}

	public Ratio multiply(Ratio other){
		// o produto de a/b x c/d é igual a ac/bd

		BigInteger ac = this.numerator.multiply(other.numerator);
		BigInteger bd = this.denominator.multiply(other.denominator);

		return new Ratio(ac, bd); // não ha divisão
	}

	public Ratio divide(Ratio other){
		// a divisão de a/b  por c/d é igual
		// ao produto de a/b x d/c que é igual a ad/bc

		BigInteger ad = this.numerator.multiply(other.denominator);
		BigInteger bc = this.denominator.multiply(other.numerator);

		return new Ratio(ad, bc); // não ha divisão
	}

	public boolean isZero(){
		return numerator.signum() == 0;
	}

	public boolean isOne(){
		return numerator.compareTo(denominator) == 0;
	}

	public BigDecimal asDecimal(){
		return numerator.signum()==0
              ? BigDecimal.ZERO
              : denominator.compareTo(BigInteger.ONE) == 0
                  ? new BigDecimal(numerator)
                  : new BigDecimal(numerator).divide(
						new BigDecimal(denominator),
						15,
						RoundingMode.HALF_EVEN
					);
	}

	public int compareTo(Ratio other){
	    // a/b == c/d <=> ad == cb

		BigInteger cb = denominator.multiply(other.numerator);
		BigInteger ad = numerator.multiply(other.denominator);

        return ad.compareTo(cb);
	}

	public int hashCode(){
		return this.numerator.hashCode() + 31 * this.denominator.hashCode();
	}

	public boolean equals(Object other){
		return other instanceof Ratio && compareTo((Ratio)other) == 0;
	}
}

Repare que a única vez que dividimos o numerador pelo denominador é quando somos obrigados a retornar um valor condensado em asDecimal(). Mesmo quando comparamos dois Ratio o fazemos sem recorrer à divisão. Isto é possivel porque mantemos o denominador sempre como um numero positivo maior que zero.

Esta implementação de Ratio utiliza redução da fração usando o a implmentação de Maior Divisor Comum de BigInteger (função cgd). Isto nos ajuda a manter as frações usando os menores numeros possiveis o a ter uma representação única para todas as frações que são múltiplas. Por exemplo, 1/2 ,2/4, 4/8, etc. são todas representadas internamente como 1/2. Isto é importante para podermos implementar equals e hashCode eficientemente.

A implementação de hashCode é assimétrica de forma que x/y ( por exemplo,2/3) não tenha o mesmo hashCode que y/x (por exemplo,3/2) evitando assim uma colisão de hashCode que não nos traz vantagem.

Repare que fornecemos um método para obter o valor a partir de um double. Este tipo pode conter valores que não são numeros racionais, então temos que testar primeiro. Para converter usamos a representação em String do valor e passamos para o método que lê strings.

A conversão de doubles para outros tipos tem sempre que ser feita com muito cuidado porque é um tipo cheio de truques e exceções de uso. Esta é também uma razão porque usamos Ratio em vez de double. Temos melhor controle sobre exceções que possam acontecer.

Discussão

O padrão Ratio evita todas as operações de divisão desde que todas as operações sejam feitas com ele. Isso permite que trabalhemos sem problemas até com números que em representação decimal seriam dizimas infinitas como 1/3.

Aqui nomeamos a implementação da classe com o nome do padrão, mas numa aplicação real, usaríamos um nome mais sugestivo como Rational ou Fraction.

O padrão Ratio deve ser usando em vez de double`ou `float sempre que possivel. Especialmente para representar taxas e cotações.

Padrões associados

O padrão Ratio está diretamente associado a Value Object e a Quantity pois representa essencialmente um valor numérico. O objetivo de Ratio é prevenir o uso da operação de divisão e isso tem aplicação direta em várias áreas mas principalmente na financeira relacionando o padrão Ratio com o padrão Money já que Ration é muito útil para expressar taxas e cotações e ajudar multiplicar Money por fatores decimais sem cair em problemas de arredondamento.

Scroll to Top