/* * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Sam */ package com.caucho.quercus.lib; import com.caucho.quercus.annotation.Optional; import com.caucho.quercus.env.*; import com.caucho.quercus.module.AbstractQuercusModule; import com.caucho.quercus.module.IniDefinitions; import com.caucho.quercus.module.IniDefinition; import com.caucho.util.L10N; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.math.RoundingMode; /** * PHP math routines. */ public class BcmathModule extends AbstractQuercusModule { private static final L10N L = new L10N(BcmathModule.class); private static final BigDecimal ZERO = BigDecimal.ZERO; private static final BigDecimal ONE = BigDecimal.ONE; private static final BigDecimal TWO = new BigDecimal(2); private static final int SQRT_MAX_ITERATIONS = 50; private static final IniDefinitions _iniDefinitions = new IniDefinitions(); public String []getLoadedExtensions() { return new String[] { "bcmath" }; } /** * Returns the default php.ini values. */ public IniDefinitions getIniDefinitions() { return _iniDefinitions; } private static BigDecimal toBigDecimal(Value value) { try { if (value instanceof StringValue) return new BigDecimal(value.toString()); if (value instanceof DoubleValue) return new BigDecimal(value.toDouble()); else if (value instanceof LongValue) return new BigDecimal(value.toLong()); else return new BigDecimal(value.toString()); } catch (NumberFormatException ex) { return ZERO; } catch (IllegalArgumentException ex) { return ZERO; } } private static int calculateScale(Env env, int scale) { if (scale < 0) { Value iniValue = env.getIni("bcmath.scale"); if (iniValue != null) scale = iniValue.toInt(); } if (scale < 0) scale = 0; return scale; } /** * Add two arbitrary precision numbers. * * The optional scale indicates the number of decimal digits to include in * the result, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcadd(Env env, Value value1, Value value2, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal bd1 = toBigDecimal(value1); BigDecimal bd2 = toBigDecimal(value2); BigDecimal bd = bd1.add(bd2); bd = bd.setScale(scale, RoundingMode.DOWN); return bd.toPlainString(); } /** * Compare two arbitrary precision numbers, return -1 if value 1 < value2, * 0 if value1 == value2, 1 if value1 > value2. * * The optional scale indicates the number of decimal digits to include in * comparing the values, the default is the value of a previous call to * {@link #bcscale} or the value of the ini variable "bcmath.scale". */ public static int bccomp(Env env, Value value1, Value value2, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal bd1 = toBigDecimal(value1); BigDecimal bd2 = toBigDecimal(value2); bd1 = bd1.setScale(scale, RoundingMode.DOWN); bd2 = bd2.setScale(scale, RoundingMode.DOWN); return bd1.compareTo(bd2); } /** * Divide one arbitrary precision number (value1) by another (value2). * * A division by zero results in a warning message and a return value of null. * * The optional scale indicates the number of decimal digits to include in * the result, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcdiv(Env env, Value value1, Value value2, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal bd1 = toBigDecimal(value1); BigDecimal bd2 = toBigDecimal(value2); if (bd2.compareTo(ZERO) == 0) { env.warning(L.l("division by zero")); return null; } BigDecimal result; if (scale > 0) { result = bd1.divide(bd2, scale + 2, RoundingMode.DOWN); } else { result = bd1.divide(bd2, 2, RoundingMode.DOWN); } result = result.setScale(scale, RoundingMode.DOWN); return result.toPlainString(); } /** * Return the modulus of an aribtrary precison number. * The returned number is always a whole number. * * A modulus of 0 results in a division by zero warning message and a * return value of null. */ public static String bcmod(Env env, Value value, Value modulus) { BigDecimal bd1 = toBigDecimal(value).setScale(0, RoundingMode.DOWN); BigDecimal bd2 = toBigDecimal(modulus).setScale(0, RoundingMode.DOWN); if (bd2.compareTo(ZERO) == 0) { env.warning(L.l("division by zero")); return null; } BigDecimal bd = bd1.remainder(bd2, MathContext.DECIMAL128); // scale is always 0 in php bd = bd.setScale(0, RoundingMode.DOWN); return bd.toPlainString(); } /** * Multiply two arbitrary precision numbers. * * The optional scale indicates the number of decimal digits to include in * the result, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcmul(Env env, Value value1, Value value2, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal bd1 = toBigDecimal(value1); BigDecimal bd2 = toBigDecimal(value2); BigDecimal bd = bd1.multiply(bd2); // odd php special case for 0, scale is ignored: if (bd.compareTo(ZERO) == 0) { if (scale > 0) return "0.0"; else return "0"; } bd = bd.setScale(scale, RoundingMode.DOWN); bd = bd.stripTrailingZeros(); return bd.toPlainString(); } /** * Raise one arbitrary precision number (base) to the power of another (exp). * * exp must be a whole number. Negative exp is supported. * * The optional scale indicates the number of decimal digits to include in * the result, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcpow(Env env, Value base, Value exp, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal bd1 = toBigDecimal(base); BigDecimal bd2 = toBigDecimal(exp); if (bd2.scale() > 0) env.warning("fractional exponent not supported"); int exponent = bd2.toBigInteger().intValue(); if (exponent == 0) return "1"; boolean isNeg; if (exponent < 0) { isNeg = true; exponent *= -1; } else isNeg = false; BigDecimal bd = bd1.pow(exponent); if (isNeg) bd = ONE.divide(bd, scale + 2, RoundingMode.DOWN); bd = bd.setScale(scale, RoundingMode.DOWN); if (bd.compareTo(BigDecimal.ZERO) == 0) return "0"; bd = bd.stripTrailingZeros(); return bd.toPlainString(); } /** * Raise one arbitrary precision number (base) to the power of another (exp), * and then return the modulus. * The returned number is always a whole number. * * exp must be a whole number. Negative exp is supported. * * The optional scale indicates the number of decimal digits to include in * the pow calculation, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcpowmod(Env env, Value base, Value exp, Value modulus, @Optional("-1") int scale) { scale = calculateScale(env, scale); // XXX: this is inefficient, s/b fast-exponentiation String pow = bcpow(env, base, exp, scale); if (pow == null) return null; return bcmod(env, env.createStringOld(pow), modulus); } /** * Set the default scale to use for subsequent calls to bcmath functions. * The scale is the number of decimal points to include in the string that * results from bcmath calculations. * * A default scale set with this function overrides the value of the * "bcmath.scale" ini variable. */ public static boolean bcscale(Env env, int scale) { env.setIni("bcmath.scale", String.valueOf(scale)); return true; } /** * Return the square root of an arbitrary precision number. * * A negative operand results in a warning message and a return value of null. * * The optional scale indicates the number of decimal digits to include in * the result, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcsqrt(Env env, Value operand, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal value = toBigDecimal(operand); int compareToZero = value.compareTo(ZERO); if (compareToZero < 0) { env.warning(L.l("square root of negative number")); return null; } else if (compareToZero == 0) { return "0"; } int compareToOne = value.compareTo(ONE); if (compareToOne == 0) return "1"; // newton's algorithm int cscale; // initial guess BigDecimal initialGuess; if (compareToOne < 1) { initialGuess = ONE; cscale = value.scale(); } else { BigInteger integerPart = value.toBigInteger(); int length = integerPart.toString().length(); if ((length % 2) == 0) length--; length /= 2; initialGuess = ONE.movePointRight(length); cscale = Math.max(scale, value.scale()) + 2; } // iterate BigDecimal guess = initialGuess; BigDecimal lastGuess; for (int iteration = 0; iteration < SQRT_MAX_ITERATIONS; iteration++) { lastGuess = guess; guess = value.divide(guess, cscale, RoundingMode.DOWN); guess = guess.add(lastGuess); guess = guess.divide(TWO, cscale, RoundingMode.DOWN); if (lastGuess.equals(guess)) { break; } } value = guess; value = value.setScale(scale, RoundingMode.DOWN); return value.toPlainString(); } /** * Subtract arbitrary precision number (value2) from another (value1). * * The optional scale indicates the number of decimal digits to include in * the result, the default is the value of a previous call to {@link #bcscale} * or the value of the ini variable "bcmath.scale". */ public static String bcsub(Env env, Value value1, Value value2, @Optional("-1") int scale) { scale = calculateScale(env, scale); BigDecimal bd1 = toBigDecimal(value1); BigDecimal bd2 = toBigDecimal(value2); BigDecimal bd = bd1.subtract(bd2); bd = bd.setScale(scale, RoundingMode.DOWN); return bd.toPlainString(); } public static final IniDefinition INI_BCMATH_SCALE = _iniDefinitions.add("bcmath.scale", 0, PHP_INI_ALL); }