简单计算引擎分享

一、背景

最近,工作中经常遇到公式计算的情况,虽然都是加减乘除的简单运算,但使用比较频繁,于是,自己就趁着业余时间手写了一个仅支持加减乘除法的计算引擎,分享出来,供大家一起学习!

首先,一遇到简单计算,可能很多人都会想到Java通过JavaScript引擎调用Javascript数学函数实现计算,创建实例如下:

ScriptEngine engine = new ScriptEngineManager().getEngineByName(“JavaScript”);

虽然这种方式,直接使用起来很简单,但引擎本身启动是比较耗资源的,而且使用JavaScript的外部计算,毕竟不符合做为一个合格码农的作风,因此,干脆自己写一个,既方便自己使用,又可以随时根据场景修改!

好了,话不多说,进入正题!

二、限定场景

动手编写前,我们首先明确一些限定场景,毕竟是简单计算,不需要兼容太多内容,所以,我们做如下设定:

  • 1.设定计算公式仅支持加减乘除法的计算
  • 2.公式内分隔符采用中括号分割"[]",其他符号不行,且必须成对儿出现
  • 3.每对儿分隔符"[]"里面仅支持两个元素操作,但内部如果还存在分隔符对儿,可看作一个元素
  • 4.计算的最小因子,只支持正数和零,暂不支持负数
  • 支持举例:
    2 + 3
    2 + [ 3 + 4]
    [ 2 + 3 ] + [ 3 + 4]
    [ [ 2 + 3 ] - 3 ] * [ 3 / 4]
  • 不支持举例:
    2 + 3 + 4 // 多于两个元素
    2 + ( 3 + 4 ) // 分隔符必须是中括号
    2 + [ 3 + 4 ] + 5 // 多于两个元素

三、代码实现

代码中会使用到插件lombok,请同学们自行提前安装!

1.实体类

首先我们定义好需要操作的实体对象。这里有三个,Constant:常量符号类;Symbol:计算符号枚举类;Operator:计算公式类。

package cn.wxson.cal.model;

/**
 * Title 常量类
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
public interface Constant {
    String BRACKETS_START = "[";
    String BRACKETS_END = "]";
    String SPECIAL = "###";
}
package cn.wxson.cal.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

/**
 * Title 计算符号
 * 目前,仅支持:加减乘除
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@AllArgsConstructor
public enum Symbol {
    ADD("+", 2, "加"),
    SUBTRACT("-", 2, "减"),
    MULTIPLY("*", 1, "乘"),
    DIVIDE("/", 1, "除");
    /**
     * 计算符字符串
     */
    @Setter
    @Getter
    private String literal;
    /**
     * 计算符优先级
     */
    @Setter
    @Getter
    private int priority;
    /**
     * 计算符名称
     */
    private String name;
}
package cn.wxson.cal.model;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

/**
 * Title 公式
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@Setter
@Getter
@Builder
public class Operator {
    /**
     * 第一位表达式
     */
    private String first;
    /**
     * 第二位表达式
     */
    private String second;
    /**
     * 表达式间的计算符
     */
    private Symbol symbol;
}

2.计算类

因为我们要做加减乘除计算,所以可以根据两个计算因子来定义好计算接口Calculate。

package cn.wxson.cal.biz;

import java.math.BigDecimal;

/**
 * Title 计算
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@FunctionalInterface
public interface Calculate {

    /**
     * 计算方法
     *
     * @param first  第一个元素
     * @param second 第二个元素
     * @return 计算结果
     */
    BigDecimal eval(String first, String second);
}

在Calculate的基础上,我们来实现加法、减法、乘法、除法的运算操作。

package cn.wxson.cal.biz.impl;

import cn.wxson.cal.biz.Calculate;

import java.math.BigDecimal;

/**
 * Title 加法
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
public class Add implements Calculate {

    @Override
    public BigDecimal eval(String first, String second) {
        BigDecimal firstBd = new BigDecimal(first);
        BigDecimal secondBd = new BigDecimal(second);
        return firstBd.add(secondBd);
    }
}
package cn.wxson.cal.biz.impl;

import cn.wxson.cal.biz.Calculate;

import java.math.BigDecimal;

/**
 * Title 减法
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
public class Subtract implements Calculate {

    @Override
    public BigDecimal eval(String first, String second) {
        BigDecimal firstBd = new BigDecimal(first);
        BigDecimal secondBd = new BigDecimal(second);
        return firstBd.subtract(secondBd);
    }
}
package cn.wxson.cal.biz.impl;

import cn.wxson.cal.biz.Calculate;

import java.math.BigDecimal;

/**
 * Title 乘法
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
public class Multiply implements Calculate {

    @Override
    public BigDecimal eval(String first, String second) {
        BigDecimal firstBd = new BigDecimal(first);
        BigDecimal secondBd = new BigDecimal(second);
        return firstBd.multiply(secondBd);
    }
}
package cn.wxson.cal.biz.impl;

import cn.wxson.cal.biz.Calculate;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * Title 除法
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
public class Divide implements Calculate {

    @Override
    public BigDecimal eval(String first, String second) {
        BigDecimal firstBd = new BigDecimal(first);
        BigDecimal secondBd = new BigDecimal(second);
        return firstBd.divide(secondBd, 2, RoundingMode.HALF_UP);
    }
}

最后,计算类创建成功后,我们还需要一个计算工厂类,来根据计算符号创建具体的计算实例。

package cn.wxson.cal.factory;

import cn.wxson.cal.biz.Calculate;
import cn.wxson.cal.biz.impl.Add;
import cn.wxson.cal.biz.impl.Divide;
import cn.wxson.cal.biz.impl.Multiply;
import cn.wxson.cal.biz.impl.Subtract;
import cn.wxson.cal.model.Symbol;
import lombok.extern.slf4j.Slf4j;

/**
 * Title 算子工厂
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@Slf4j
public class OperatorFactory {

    /**
     * 根据计算符获得计算实例
     *
     * @param symbol 计算符
     * @return 计算实例
     */
    public static Calculate createCal(Symbol symbol) {
        switch (symbol) {
            case ADD:
                return new Add();
            case SUBTRACT:
                return new Subtract();
            case MULTIPLY:
                return new Multiply();
            case DIVIDE:
                return new Divide();
            default:
                log.error("[不存在该计算符] [计算符:{}]", symbol);
                throw new NullPointerException("[不存在的计算符]");
        }
    }
}

3.工具类

在正式进行计算公式解析前,我们来创建两个工具类,辅助我们后续操作,分别是字符操作工具:StringUtil,计算辅助工具:FormulaUtil。

package cn.wxson.cal.util;

import cn.wxson.cal.model.Constant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.Stack;

/**
 * Title 字符串操作
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@Slf4j
public final class StringUtil {

    /**
     * 分隔符
     */
    public static final String[] SRC_BRACKETS = {Constant.BRACKETS_START, Constant.BRACKETS_END};
    /**
     * 清洗分隔符为空的集合
     */
    public static final String[] DESC_BRACKETS = {StringUtils.EMPTY, StringUtils.EMPTY};
    /**
     * 字符:[
     */
    public static final char SC = Constant.BRACKETS_START.toCharArray()[0];
    /**
     * 字符:]
     */
    public static final char EC = Constant.BRACKETS_END.toCharArray()[0];

    /**
     * 从开始分隔符下标开始,获取与其对应的结束分隔符索引
     * 这里使用stack结构,便于标识多重分隔符号场景下,找到目标结束符
     *
     * @param formula 公式
     * @param index   开始分割符的下标,即"["的下标
     * @return 与开始分隔符"["相对应的结束分隔符"]"的下标
     */
    public static int index(String formula, int index) {
        Stack<Integer> stack = new Stack<>();
        char[] chars = formula.toCharArray();
        for (int i = index + 1; i < chars.length; i++) {
            char c = chars[i];
            if (c == SC) {
                stack.push(i);
            } else if (c == EC) {
                if (stack.empty()) {
                    return i;
                } else {
                    stack.pop();
                }
            }
        }
        return -1;
    }

    /**
     * 清洗字符串
     *
     * @param value 字符串值
     * @return 清洗结果
     */
    public static String clean(String value) {
        return trim(replace(value));
    }

    /**
     * 去除字符串两端空字符
     *
     * @param value 字符串值
     * @return 结果
     */
    public static String trim(String value) {
        return StringUtils.trim(value);
    }

    /**
     * 替换字符串中的分隔符为空
     *
     * @param value 字符串值
     * @return 替换结果
     */
    public static String replace(String value) {
        return StringUtils.replaceEach(value, SRC_BRACKETS, DESC_BRACKETS);
    }
}
package cn.wxson.cal.util;

import cn.wxson.cal.model.Symbol;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Optional;

/**
 * Title 计算辅助工具
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@Slf4j
public final class FormulaUtil {

    /**
     * 根据计算符字符串,获取计算符对象
     *
     * @param formula 公式
     * @return 计算符对象
     */
    public static Symbol symbol(String formula) {
        Optional<Symbol> first = Arrays.stream(Symbol.values()).filter(symbol -> StringUtils.contains(formula, symbol.getLiteral())).findFirst();
        if (!first.isPresent()) {
            log.error("[公式内不包含任何计算符] [公式:{}]", formula);
            throw new NullPointerException("[公式内不包含任何计算符]");
        }
        return first.get();
    }

    /**
     * 根据计算公式,判断是否只有一个计算符
     *
     * @param formula 公式
     * @return 判断结果
     */
    public static boolean isSingleSymbol(String formula) {
        return countSymbol(formula) == 1;
    }

    /**
     * 根据计算公式,获取计算符个数
     *
     * @param formula 公式
     * @return 个数
     */
    public static int countSymbol(String formula) {
        return Arrays.stream(Symbol.values()).map(symbol -> StringUtils.countMatches(formula, symbol.getLiteral())).reduce(Integer::sum).orElse(0);
    }

    /**
     * 根据计算公式,判断是否存在计算符
     *
     * @param formula 公式
     * @return 判断结果
     */
    public static boolean noSymbol(String formula) {
        Optional<Symbol> any = Arrays.stream(Symbol.values()).filter(symbol -> StringUtils.contains(formula, symbol.getLiteral())).findAny();
        return !any.isPresent();
    }
}

4.解析类

最后一步,我们根据计算公式来解析出最后的计算结果,结果我们定义为BigDecimal,计算公式以字符串形式传入。解析接口如下:

package cn.wxson.cal.biz;

import java.math.BigDecimal;

/**
 * Title 解析
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@FunctionalInterface
public interface Parser {

    /**
     * 该解析方法,适用场景有如下限制:
     * 1.公式目前仅支持加减乘除法计算
     * 2.公式内分隔符采用"[]",其他符号不行,且必须成对儿出现
     * 3.每对儿分隔符"[]"里面仅支持两个元素操作,但内部如果还存在分隔符对儿,可看作一个元素
     * 4.计算最小因子,只支持正数和零,暂不支持负数
     * 支持举例:
     * 2 + 3
     * 2 + [ 3 + 4]
     * [ 2 + 3 ] + [ 3 + 4]
     * [ [ 2 + 3 ] - 3 ] * [ 3 / 4]
     * 不支持举例:
     * 2 + 3 + 4
     * 2 + [ 3 + 4 ] + 5
     * 2 + ( 3 + 4 )
     *
     * @param formula 公式,例如:[ 2 + 3 ] - 1 或 [ 2 + 3 ] - [1 + 4]
     * @return 解析后的计算值
     */
    BigDecimal parse(String formula);
}

具体的解析实例会稍微复杂点儿,其中会用到两次递归操作,不过,对于两三年编程经验的人来说,就是小菜一碟儿了。

package cn.wxson.cal.biz.impl;

import cn.wxson.cal.biz.Calculate;
import cn.wxson.cal.biz.Parser;
import cn.wxson.cal.factory.OperatorFactory;
import cn.wxson.cal.model.Constant;
import cn.wxson.cal.model.Operator;
import cn.wxson.cal.model.Symbol;
import cn.wxson.cal.util.FormulaUtil;
import cn.wxson.cal.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.math.BigDecimal;

/**
 * Title 公式解析
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
@Slf4j
public class ParserImpl implements Parser {

    /**
     * 该解析方法,适用场景有如下限制:
     * 1.公式目前仅支持加减乘除法计算
     * 2.公式内分隔符采用"[]",其他符号不行,且必须成对儿出现
     * 3.每对儿分隔符"[]"里面仅支持两个元素操作,但内部如果还存在分隔符对儿,可看作一个元素
     * 4.计算最小因子,只支持正数和零,暂不支持负数
     * 支持举例:
     * 2 + 3
     * 2 + [ 3 + 4]
     * [ 2 + 3 ] + [ 3 + 4]
     * [ [ 2 + 3 ] - 3 ] * [ 3 / 4]
     * 不支持举例:
     * 2 + 3 + 4
     * 2 + [ 3 + 4 ] + 5
     * 2 + ( 3 + 4 )
     *
     * @param formula 公式,例如:[ 2 + 3 ] - 1 或 [ 2 + 3 ] - [1 + 4]
     * @return 解析后的计算值
     */
    @Override
    public BigDecimal parse(String formula) {
        // 1.不包含计算符号时,为单数字
        if (FormulaUtil.noSymbol(formula)) {
            String value = StringUtil.clean(formula);
            return new BigDecimal(value);
        }
        // 2.单符号计算时,为直接表达式,例如:2 + 1
        if (FormulaUtil.isSingleSymbol(formula)) {
            String value = StringUtil.clean(formula);
            return directEval(value);
        }
        // 3.多符号计算情况下,前后都是间接表达式,例如:[ 2 + 1 ] + 1    或   [ 2 + 1] + [ 2 + 1 ]
        Operator operator = parseTo(formula);
        BigDecimal first = parse(operator.getFirst());
        BigDecimal second = parse(operator.getSecond());
        Calculate cal = OperatorFactory.createCal(operator.getSymbol());
        return cal.eval(first.toString(), second.toString());
    }

    /**
     * 对多符号计算公式进行解析
     *
     * @param formula 公式,例如:1 + [ 2 + 3]    或    [ 1 + 2 ] + [ 2 + 3]
     * @return 解析结果
     */
    private Operator parseTo(String formula) {
        int startIndex = StringUtils.indexOf(formula, Constant.BRACKETS_START);
        if (startIndex == -1) {
            log.error("[多符号计算情况下,不存在分隔符\"[]\"] [表达式:{}]", formula);
            throw new RuntimeException("[多符号计算情况下,不存在分隔符\"[]\"]");
        }
        int endIndex = StringUtil.index(formula, startIndex);
        String first = StringUtils.substring(formula, startIndex + 1, endIndex);
        String replace = StringUtils.replace(formula, Constant.BRACKETS_START + first + Constant.BRACKETS_END, Constant.SPECIAL);
        int startIndex2 = StringUtils.indexOf(replace, Constant.BRACKETS_START);
        if (startIndex2 == -1) {
            Symbol symbol = FormulaUtil.symbol(replace);
            String second = StringUtils.replaceEach(replace, new String[]{Constant.SPECIAL, symbol.getLiteral()}, new String[]{StringUtils.EMPTY, StringUtils.EMPTY});
            return Operator.builder().first(first).second(second).symbol(symbol).build();
        }
        int endIndex2 = StringUtil.index(replace, startIndex2);
        String second = StringUtils.substring(replace, startIndex2 + 1, endIndex2);
        String replace2 = StringUtils.replace(replace, Constant.BRACKETS_START + second + Constant.BRACKETS_END, Constant.SPECIAL);
        Symbol symbol = FormulaUtil.symbol(replace2);
        return Operator.builder().first(first).second(second).symbol(symbol).build();
    }

    /**
     * 对单符号公式进行直接计算
     *
     * @param formula 公式
     * @return 计算结果
     */
    public static BigDecimal directEval(String formula) {
        Symbol symbol = FormulaUtil.symbol(formula);
        int index = StringUtils.indexOf(formula, symbol.getLiteral());
        String first = StringUtils.substring(formula, 0, index);
        String second = StringUtils.substring(formula, index + 1);
        Calculate cal = OperatorFactory.createCal(symbol);
        return cal.eval(first, second);
    }
}

5.测试

最后代码编写完毕,我们来创建个测试类来实验下效果。

package cn.wxson.cal.test;

import cn.wxson.cal.biz.Parser;
import cn.wxson.cal.biz.impl.ParserImpl;

import java.math.BigDecimal;

/**
 * Title 测试类
 *
 * @author Ason(18078490)
 * @date 2020-07-24
 */
public class Domain {

    public static void main(String[] arg) {
        String formula = "  [[20*2]+[[1-3] *2] ] / 4 ";
        Parser parser = new ParserImpl();
        BigDecimal result = parser.parse(formula);
        System.out.println(result.toString());
    }
}

通过上面的测试类,我们可以发现,计算公式" [[20*2]+[[1-3] *2] ] / 4 "故意被我写的复杂些,其中有多层嵌套,而且包含了加减乘除各种运算,还有空格等,我们来看计算结果:
计算结果
怎么样?是不是你期望的答案?😊

四、总结

通过这个简单计算引擎的编写,我们可以发现,其实这种编程并不难,其中比较复杂的部分就是公式解析功能,只要提前设定好你的引擎使用场景,相信也不会难到你!

另外,对于复杂计算引擎的考虑,目前还在预想中,今后做好,也会分享给大家,敬请期待!
😄

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值