# 项目简介
本项目目的是写一个计算器项目,主要考察算法(栈、后缀表达式)和基本的HTML DOM,JavaScript的应用。本项目的基本架构是SpringBoot+Thymeleaf+Ajax。本项目由五个计算器组成:第一个是商务计算器,主要实现加减乘除的连续运算,没有优先级,给出第一个操作数,按下操作符,清空输入栏,然后输入第二个操作数,按下等于会显示结果;第二个是四则计算器,涉及到带有优先级的运算,能够实现一个带括号的四则表达式的运算;第三个是科学计算器,就是在四则计算器的基础上添加了三角、反三角、指数运算;第四个是贷款计算器,可以由两种方式(等额本金和等额本息,具体计算公式请百度)计算每月还款等数据;第五个是进制转换计算器,可以实现二进制、八进制、十进制、十六进制之间的相互转化。 以下内容仅包括算法解析。代码中已经添加了足够的注释,具体项目源码请访问GitHub: https://github.com/Nown1/calculator (opens new window)
# 商务计算器
商务计算器(business.html)主要完成加减乘除的连续运算,例如输入25,*,2,=会输出50,接着按 *,2 会输出100;算法:首先用HTML DOM拼操作数,这里我用了一个p标签来记录输入的数字。每按下一个数字就把这个数字拼接上。p.innerHTML += obj.value;
当按下操作符,要把第一个操作数字符串保存到res里面,同时保存操作符,将p标签内容清空,然后继续输入数字,之后按下等于号时开始计算,首先把第二个操作数记录下来。由于之前我们记录了操作符和第一个操作数,只要进行运算即可。比如第一个操作数是res=“25”
,即result=25.0;
按下操作符operator=‘ * ‘
,第二个操作数是num=“2”
,即number=2.0
;然后进行计算,result+=number
,所以result==50.0
,然后显示在p标签内。如果要继续运算,按下操作符就会把之前的操作符替换掉,结果仍然保存在result里面。然后继续输入数字会保存在num里面,然后就可以再次运算。清零按钮注意要把result一并清除,退格按钮只要截取p标签里面的内容。代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>商务计算器</title>
<style>
body{
background-image: url("../img/2.jpg");
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
.panel {
/*定义面板*/
border: 4px solid #ddd;
width: 440px;
height: 420px;
margin: auto;
/*border-radius: 6px;*/
}
.panel p, .panel input {
/*定义字体*/
font-family: "微软雅黑";
font-size: 20px;
margin: 4px;
float: left;
/*border-radius: 4px;*/
}
.panel p {
/*定义输入框*/
width: 410px;
height: 26px;
border: 1px solid #ddd;
padding: 6px;
overflow: hidden;
}
.panel input {
/*按钮大小*/
width: 100px;
height: 50px;
border:1px solid #ddd;
align-content: center;
}
</style>
<script>
window.onload = function (){
//获取事件目标节点(整个div)
var div = document.getElementById("jsq");
//给事件目标节点后绑定事件
var num="0";//记录操作数
var res="0";//记录结果
var operator=" ";//记录运算符
var p=document.getElementById("screen");//获取显示屏
function calculate(){//执行计算操作
//1.将 "0"+屏幕显示的数字先存到num里面,
//2.把num,res分别化为float数number,result
//3.执行运算
num="0"+p.innerHTML;
var number =parseFloat(num);
var result=parseFloat(res);
if(operator=="+"){
result+=number;
operator="";
}else if(operator=="-"){
result-=number;
operator="";
}else if(operator=="*"){
result*=number;
operator="";
}else if(operator=="/"){
if(number==0){
result="Error";
operator="";
}
result/=number;
operator="";
}else {
result=number;
operator="";
}
// alert(num+";"+number+";"+res+";"+result+";"+operator);
return result;
}
function setOperator(a){//置运算符
//需要执行:1.将符号复制给operator变量
// 2.将 "0"+显示屏字符串 赋值给res
// 3.将显示屏清空
operator=a;
res="0"+p.innerHTML;
p.innerHTML="";
}
function clear(){//清零方法
p.innerHTML="";
num="0";
res="0";
}
function back(){//退格方法
p.innerHTML=p.innerHTML.slice(0,p.innerHTML.length-1);
}
function setStr(a){//置数方法,需要执行以下步骤:
//1.将
p.innerHTML+=a;
}
div.onclick = function (e){
//获取事件源
var obj = e.srcElement||e.target;
//获取显示屏
// var p = document.getElementById("screen");
//若用户点击的区域是input便执行以下方法
if(obj.nodeName=="INPUT"){
if(obj.value=="C"){
// p.innerHTML = "";
clear();
}else if(obj.value=="DEL"){
// p.innerHTML=p.innerHTML.slice(0,p.innerHTML.length-1)
back();
}else if(obj.value=="="){
p.innerHTML = calculate().toString();
}else if(obj.value=="+"||obj.value=="-"||obj.value=="*"||obj.value=="/"){
setOperator(obj.value);
}else{
p.innerHTML += obj.value;
}
}
}
}
</script>
</head>
<body>
<form action="/">
<a href="/Business">商务计算器</a><br/>
<a href="/Simple" >四则计算器</a><br/>
<a href="/Science">科学计算器</a><br/>
<a href="/Loan">贷款计算器</a><br/>
<a href="/Base">进制计算器</a>
</form>
<div class="panel" id="jsq">
<div>
<h3 align="center">商务计算器</h3>
<hr>
<p id="screen"></p>
<input type="button" value="C" onclick="clear()">
<input type="button" value="DEL" onclick="back()">
<div style="clear:both"></div>
</div>
<div>
<input type="button" value="7">
<input type="button" value="8">
<input type="button" value="9">
<input type="button" value="/">
<input type="button" value="4" >
<input type="button" value="5" >
<input type="button" value="6" >
<input type="button" value="*" >
<input type="button" value="1" >
<input type="button" value="2" >
<input type="button" value="3" >
<input type="button" value="-">
<input type="button" value="0" >
<input type="button" value="." >
<input type="button" value="=">
<input type="button" value="+">
<div style="clear:both"></div>
</div>
</div>
</body>
</html>
# 四则计算器
四则计算器(simple.html)要涉及到优先级运算,要用到后缀表达式,栈。因此我并没有用JavaScript写算法,而是用Java。在程序运行时,前端只要关注如何把表达式传到后端,后端进行运算,并将结果发送到前端。既然涉及到不刷新页面的访问请求,所以用到了Ajax异步请求。在向后端发送请求时,遇到了一个问题,就是由于编码问题,某些字符,如+、-、*、/等传到后端就消失了,因此这里又进行了强制编码var url="/Simple?question=" +encodeURI(encodeURIComponent(p.innerHTML));
后端在接受请求时需要解码String q=java.net.URLDecoder.decode(question,"UTF-8");
,注意用try-catch语句捕获异常。前端关键代码如下:
<script>
window.onload = function () {
//获取事件目标节点(整个div)
var div = document.getElementById("jsq");
//获取输入栏
var p=document.getElementById("question");
//获取答案栏
var q=document.getElementById("answer");
var ans;
//使用Ajax发送异步请求获取结果并修改答案栏文字
function calculate()
{
var xmlhttp;
if (window.XMLHttpRequest)
{
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp=new XMLHttpRequest();
}
else
{
// IE6, IE5 浏览器执行代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
q.innerHTML=xmlhttp.responseText;
}
}
//直接令url="/Simple?question=" +p.innerHTML 会导致url中部分字符丢失,比如+ -
//使用encodeURI(encodeURIComponent(url))方法可以将url转化为utf-8编码形式,
//用%2F代替/,用%2B代替+,但是在请求服务器的时候,请求路径前面必须是/,
//因此只要把p.innerHTML进行编码即可,然后和请求路径拼接起来
//后端接受到的数据是utf-8编码的,因此还要进行解码
//方法是:String q=java.net.URLDecoder.decode(question,"UTF-8");
//注意用try-catch捕获异常
var url="/Simple?question=" +encodeURI(encodeURIComponent(p.innerHTML));
xmlhttp.open("POST",url,true);
xmlhttp.send();
}
div.onclick = function (e){
//获取事件源
var obj = e.srcElement||e.target;
//若用户点击的区域是input便执行以下方法
if(obj.nodeName=="INPUT"){
if(obj.value=="C"){
p.innerHTML = "";
q.innerHTML="";
}else if (obj.value=="M"){
ans=q.innerHTML;
}else if (obj.value=="ans"){
p.innerHTML=ans;
}
else if(obj.value=="DEL"){
p.innerHTML=p.innerHTML.slice(0,p.innerHTML.length-1)
}else if(obj.value=="="){
calculate();
}else{
p.innerHTML += obj.value;
}
}
}
}
</script>
后端我是用Java写的算法,我们先来学习一下算法的知识: 本算法涉及栈(先进后出,后进先出)的结构,由于Java是有多种数据类型的,所以我设计了两个栈,一个元素类型是char,用于存放运算符,一个元素类型是double,用于存放数据。当然,会泛型的朋友可以直接用泛型,我菜鸡不说话。同时,在计算时要用到后缀表达式,例如1+((2+3)*4+7)+6/4换成后缀表达式应该是1,2,3,+,4,*,7,+,+,6,4,/,+。这样在计算的时候如果是数字先保存,遇到运算符就取出栈顶两个数字进行一次运算,然后将结果再压入栈内。计算过程如下:
1 //遇到数字,直接进栈
1,2 //遇到数字,直接进栈
1,2,3 //遇到数字,直接进栈
1,5 //遇到加号,取出栈顶两个数进行加法运算,2+3=5,将5进栈
1,5,4 //遇到数字,直接进栈
1,20 //遇到*,取出栈顶两个数进行乘法运算,5*4=20,将20进栈
1,20,7 //遇到数字,直接进栈
1,27 //遇到+,取出栈顶两个数进行加法运算,20+7=27,将27进栈
28 //遇到+,取出栈顶两个数进行加法运算,1+27=28,将28进栈
28,6 //遇到数字,直接进栈
28,6,4 //遇到数字,直接进栈
28,6,4 //遇到/,取出栈顶两个数进行除法运算,6/4=1.5,将1.5进栈
28,1.5 //遇到+,取出栈顶两个数,28+1.5=29.5,将29.5进栈
29.5//计算结束,数字栈内栈顶元素就是结果
那么,在得到后缀表达式的时候,什么时候让操作符进栈,什么时候出栈呢?这就要考虑优先级,如果要进栈运算符优先级高就进栈,否则取出栈顶操作符执行运算,直到把这个运算符放进去
先来个简单的,对于1+2+3,我们是直接从左到右进行运算的,数字1记下,第一个运算符加号入栈,数字2记下,又遇到加号,我们希望先把前面两个数加起来,因此要先把栈内的加号取出来记录,也就是进行了一次加法运算,然后把第二个加号入栈,数字3记下,遍历结束,把栈内的加号取出来补上,就是1,2,+,3,+
对于1+2*3,数字1先记下,然后第一个操作符+先进栈,数字2记下,碰到乘号,我们并不想让前两个数进行运算,而是让乘号进栈,然后将3取出,遍历结束,再把操作符从栈内取出补上,得到了1,2,3,*,+(加号先进栈,所以后出来)
对于2*3+1,数字2记下,然后第一个乘号入栈,然后数字3记下,遇到加号,我们希望2和3相乘,所以先把栈内的乘号取出来,之后再把加号放入栈内,然后再把1记下,最后把栈内的操作符取出来补上,就得到了2,3,*,+
如果有括号,比如1+(2+3)* 6,第一个数1记下,第一个运算符加号进栈,左括号先进栈,数字2记下,我们不希望1和2相加,因此让加号进栈,数字3记下,遇到右括号,我们希望先把括号内的算完,所以先把栈顶的加号取出来,也就是先把栈顶的2和3进行了一次加法运算,然后栈顶元素是左括号,我们就把左括号取出,右括号至此处理完毕。此时栈顶元素是加号,遇到乘号,先把乘号进栈,数字6取出来,然后把栈内的操作符都取出来,得到的就是1,2,3,+,6,*,+
由此可见,栈内的加号(或减号)优先级比栈外的加号(或减号)优先级高,比栈外的乘号(或除号)优先级低;栈内的乘号(或除号)优先级比栈外的加号(或减号)优先级高,比栈外的乘号(或除号)优先级高。栈外的左括号优先级最高,但是一旦进栈就成为最低,比栈外加号优先级还低。栈外的右括号优先级最低,比栈内的加号优先级还低。为了便于判断什么时候栈外的右括号和栈内的左括号匹配,我们应该让两者优先级相等。同时,为了便于对操作符栈进行初始化,我们先让 # 进栈,代表最低优先级的操作符。如果用int icp(char operator)
函数返回栈外运算符operator的优先级,用 int isp(char operator)
函数返回栈内运算符优先级,则有以下关系:
icp(#)<icp( 右括号 )=isp(左括号)<icp(+)=icp(-)<isp(+)<isp(-)<icp(*)<icp(/)<isp(*)=isp(/)<icp(左括号)
注意栈外的右括号是不可能进栈的,它的作用仅限于将栈内匹配的左括号移除,代码实现如下:
public int isp(char ch) {//判断站内操作符优先级
switch (ch) {
case '#':
return 0;
case '(':
return 1;
case '*':
case '/':
return 5;
case '+':
case '-':
return 3;
default:
return -1;
}
}
public int icp(char ch) {//判断要进站操作符优先级
switch (ch) {
case '#':
return 0;
case '(':
return 6;
case '*':
case '/':
return 4;
case '+':
case '-':
return 2;
case ')':
return 1;
default:
return -1;
}
}
如果转后缀表达式再计算,需要把数字和操作符都以String的形式存到一个String数组里面,而不是简单拼接,因为简单拼接你不知道后缀表达式中相邻两个数字在哪里分开。举个简单例子,1+(2+3)* 6后缀表达式是 1,2,3,+,6,*,+,如果直接拼接就是 123+6*+,第一个数字123嘛?!而如果存到数组里面还要进行第二次遍历,时间复杂度会大一点。所以在实际算法中,我并不是先的到后缀表达式然后再计算,而是在遍历过程中就开始计算。基本思想是:
如果是一个数字,我就保存起来,如果是运算符,如果要进栈的运算符优先级高则进栈,如果要进栈的运算符优先级低就取出数字栈栈顶两个数字和操作符栈栈顶一个操作符进行运算,将结果压入数字栈内,直到这个运算符进栈,或者右括号找到与之匹配的左括号。
先对表达式字符串进行遍历,用ch代表字符串中某个字符,用String类型的number来存数字,考虑到有负数运算,先让number=“0”,用boolean类型的notComplete判断是否已经将运算符处理好,每次默认为true。如果是0~9的数字或者小数点就直接和number拼接即可,也就是number=number+ch。如果是运算符,需要先把number代表的数字存到数字栈内,然后判断优先级,如果ch优先级高则进栈,同时再把number复位,complete=false跳出循环。如果ch低则要进行运算,直到入栈。如果是右括号,则直到找到栈内对应的左括号,然后将notComplete=false。
你以为这就完了?大意了!
如果只要不是数字或小数点就把number压入栈内,当碰到左括号或者右括号后面的操作符(如果有的话)就会错误的把0压入栈内,比如2*(2+3),遇到乘号时就把2进栈,number=“0”了,当又遇到乘号时,会误把0入栈;或者(1+2)+3,遇到右括号时2入栈,然后number=“0”,当又遇到+号时又把0进栈了。因此要对这两个符号分开处理,如果是左括号只要进栈就好了,不要再把number压入数字栈。如果是右括号,则将number压入栈内,然后和栈顶元素比较优先级,直到找到对应的左括号,将之移除。处理完右括号,还要再处理右括号后面的操作符。如果此右括号是表达式最后一个字符,就结束了;如果不是,则要对右括号后面的运算符提前进行处理,因此我又引入了一个Boolean类型的flag变量,功能和notComplete类似。然而这也还没完,因为虽然表达式遍历完了,但是操作符栈内可能还有运算符。因此还要执行一些运算操作。只要运算符栈不空就取出栈顶元素和数字栈内两个数字进行运算。
年轻人,是不是草率了?
理论上讲,我的代码没有问题,可是你给我的表达式可能会有问题,比如你给我一个0....2,或者1/0,或者1+2(2-3)是什么意思?第一个这么多小数点干嘛?害得我在转double值的时候可能会出错;第二个分母为零;第三个括号前没有运算符,你说是乘号给省略了,呵呵,我不认,年轻人不讲码德!因此我又用try-catch语句捕获了一下异常。
为了便于追踪计算的每个步骤,我在执行入栈和出栈方法时都会输出一下是第几个元素,哪个元素进栈或者出栈了。在遍历结束后会输出 “遍历结束”,在执行运算时会输出执行了什么运算。
巴巴了这么多,嘴类,手累,代码才是最好的沟通语言,不说了,上代码!
# 代码展示
操作符栈(CharStack.java):
package com.nown.utils;
public class CharStack {
private int top;
private char[] elements;
public CharStack(int size){
top=-1;
elements=new char[size];
}
public char remove(){//退栈方法,从栈中取出栈顶元素并移除
System.out.println("将第"+top+"个运算符"+elements[top]+"移除运算符栈");
return elements[top--];
}
public char pop(){//显示栈顶元素,只显示而不移除
return elements[top];
}
public void add(char value){
System.out.println("第"+(top+1)+"个入栈的运算符是"+value);
elements[++top]=value;
}
public boolean isEmpty(){
return top==0;
}
}
数字栈(DoubleStack.java):
package com.nown.utils;
public class DoubleStack {
private int top;
private double[] elements;
public DoubleStack(int size){
top=-1;
elements=new double[size];
}
public double remove(){
System.out.println("将第"+top+"个数字"+elements[top]+"移除数字栈");
return elements[top--];
}
public void add(double value){
System.out.println("第"+(top+1)+"个入栈的数字是"+value);
elements[++top]=value;
}
public boolean isEmpty(){
return top==0;
}
}
简单计算器类 (SimpleCaculator):Too easy too simple
package com.nown.utils.calculator;
import com.nown.utils.CharStack;
import com.nown.utils.DoubleStack;
public class SimpleCalculator {
private CharStack charStack;//字符栈,存放运算符
private DoubleStack doubleStack;//数栈,存放运算符
private char[] question;//字符数组,将计算式转化为charArray以便进行遍历。
int length;
public SimpleCalculator(String question) {
this.question = question.toCharArray();
length = question.length();
charStack = new CharStack(length);
doubleStack = new DoubleStack(length);
charStack.add('#');//先在操作符栈存放一个#代表最低优先级运算符
doubleStack.add(0.0);//在数栈存放0进行初始化,防止"0.5"用".5"表示时报错,同时也是为了负数运算
}
public String getAnswer() {
int i = 0;
String number = "0";//临时存放数字,到运算符时则意味着这个数字结束,编程double值存到数栈
boolean notComplete;//判断有没有将操作符压入栈内
try {
for (;i<length;i++){
char ch = question[i];
notComplete=true;//没有完成进栈操作?yes
if (ch == '(') {
//如果是左括号直接进栈即可
charStack.add(ch);
notComplete = false;
} else if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == ')') {
//如果是操作符或右括号
//先把number记录的数字压入数字栈,然后把number="0"进行初始化
double num = Double.parseDouble(number);
number = "0";
doubleStack.add(num);
while (notComplete) {
if (icp(ch) > isp(charStack.pop())) {
//如果要进栈的操作符优先级高
//则将数字进栈,运算符进栈
charStack.add(ch);
notComplete = false;//没有完成压栈操作?no,完成了
} else if (icp(ch) < isp(charStack.pop())) {
//如果要进栈操作符优先级低,则从栈中取出一个操作符和两个操作数进行计算
//将结果压入数字栈,然后接着判断(不需要让notComplete=false),直到该操作符进栈
char operator = charStack.remove();
double num1 = doubleStack.remove();
double num2 = doubleStack.remove();
double result = calculate(num2, num1, operator);
doubleStack.add(result);
} else {
//当要进栈的操作符优先级和栈顶元素优先级相等,
//则只可能是栈顶元素是左括号,要进栈元素是右括号
if (i == length - 1) {
//如果此时右括号刚好是表达式最后一个字符,则只要将左括号移除
charStack.remove();
notComplete=false;
} else {
//如果不是在表达式末尾,需要提前对右括号后面的操作符进行处理,
//然后i++跳过后面的操作符
//这样做的目的是防止后面的操作符使数字栈多存入一个0
charStack.remove();
char operator = question[++i];
boolean flag = true;//设置一个临时变量判断有没有将右括号后面的操作符入栈
while (flag) {
if (icp(operator) > isp(charStack.pop())) {
//如果该操作符优先级比较高,则直接入栈即可,操作结束
charStack.add(operator);
flag = false;
} else {
//如果该操作符优先级比较低,则取出一个操作符,两个操作数进行运算,
//然后接着判断(先不让flag=false),直到该操作符进栈
char op=charStack.remove();
double num1 = doubleStack.remove();
double num2 = doubleStack.remove();
double result = calculate(num2, num1, op);
doubleStack.add(result);
}
}
notComplete=false;
}
}
}
} else {
number += ch;
}
}
//整个算式已经遍历完,但是最后一个数字可能还没进栈,且字符栈内可能还有运算符,也就是说还没有运算结束
//如果最后一个字符是右括号,那么所有的数字都已经进栈了,
// 不需要把最后的number="0"压入栈内,否则需要压入栈内
if(question[length-1]!=')'){
System.out.println("遍历后将数字"+Double.parseDouble(number)+"压入栈内");
doubleStack.add(Double.parseDouble(number));
}
System.out.println("完成遍历");
while (!charStack.isEmpty()) {
char operator = charStack.remove();
double num1 = doubleStack.remove();
double num2 = doubleStack.remove();
double result=calculate(num2,num1,operator);
doubleStack.add(result);
}
}catch( Exception e){
return "错误";
}
double answer = doubleStack.remove();
return""+answer;
}
public double calculate(double num2,double num1,char operator) throws Exception{
switch (operator){
case '+':
System.out.println("执行了一次加法:"+num2+"+"+num1);
return num2+num1;
case '-':
System.out.println("执行了一次减法:"+num2+"-"+num1);
return num2-num1;
case '*':
System.out.println("执行了一次乘法:"+num2+"*"+num1);
return num2*num1;
default:
if (num1==0){
throw new Exception();
}
System.out.println("执行了一次除法:"+num2+"/"+num1);
return num2/num1;
}
}
public int isp(char ch) {//判断站内操作符优先级
switch (ch) {
case '#':
return 0;
case '(':
return 1;
case '*':
case '/':
return 5;
case '+':
case '-':
return 3;
default:
return -1;
}
}
public int icp(char ch) {//判断要进站操作符优先级
switch (ch) {
case '#':
return 0;
case '(':
return 6;
case '*':
case '/':
case '%':
return 4;
case '+':
case '-':
return 2;
case ')':
return 1;
default:
return -1;
}
}
}
# 计算器进阶---科学计算器
以上代码只是实现了四则运算,还有括号,但是我怎么会满足呢?给爷敲!
于是,我在以上计算器基础上又加入了三角,反三角,指数运算。其实主要是优先级的问题,还有就是啊,sin,cos,tan,arcsin,arccos,arctan都是单目运算符,它前面是没有操作数的,而且运算的时候也只要取出一个操作数即可。其他的,理论上讲,大同小异。优先级可以自己推导,我就不举例说明了,有以下关系:
icp(#)<icp( 右括号 )=isp(左括号)<icp(+)=icp(-)<isp(+)<isp(-)<icp(*)<icp(/)<isp(*)=isp(/)<icp(^)=icp(sin)=icp(arcsin)<isp(^)=isp(sin)=isp(arcsin)<icp(左括号)
简单来说就是,栈内栈外加减号优先级一致,乘除号优先级一致,指数,三角,反三角优先级一致。同一操作符,栈内的优先级高于栈外操作符一等。左括号变化最大,栈外优先级最高,而栈内优先级最低。为了便于匹配左右括号,我们让栈内的左括号和栈外的右括号优先级一致,同时栈内不存在右括号。
然后对代码进一步优化。上面的代码每次在执行运算操作的时候都要从栈内remove一个运算符,两个数字,多次重复。对于这个进阶版科学计算器,还要涉及单目运算符,只有一个操作数,如果每次都在getAnswer()方法中进行判断必定会出现大量重复代码,这我能忍?盘它!
代码如下:
package com.nown.utils.calculator;
import com.nown.utils.CharStack;
import com.nown.utils.DoubleStack;
public class ScienceCalculator {
private CharStack charStack;//字符栈,存放运算符
private DoubleStack doubleStack;//数栈,存放运算符
private String question;//字符数组,将计算式转化为charArray。
int length;
public ScienceCalculator(String question) {
this.question = question;
length = question.length();
charStack = new CharStack(length);
doubleStack = new DoubleStack(length);
charStack.add('#');//先在操作符栈存放一个#代表最低优先级运算符
doubleStack.add(0.0);//在数栈存放0进行初始化,防止"0.5"用".5"表示时报错
}
public String getAnswer() {
int i = 0;
String number = "0";//临时存放数字,到运算符时则意味着这个数字结束,编程double值存到数栈
boolean notComplete;//判断有没有将操作符压入栈内
try {
for (;i<length;i++){
char ch = question.charAt(i);
notComplete=true;//没有完成进栈操作?yes
if (ch == '('||ch=='s'||ch=='c'||ch=='t'||
ch=='i'||ch=='o'||ch=='n'||ch=='q'||ch=='a') {
//如果是左括号或者单目运算符直接进栈即可
charStack.add(ch);
notComplete = false;
}
else if ((ch>='0'&&ch<='9')||ch=='.'||ch=='p'){
//如果是数字则只要和number拼接即可
//如果是PI,则直接把"3.141...."赋值给number
if (ch=='p'){
number=Double.toString(Math.PI);
}else {
number += ch;
}
}else {
//如果是双目运算符或右括号
//先把number记录的数字压入数字栈,然后把number="0"进行初始化
double num = Double.parseDouble(number);
number = "0";
doubleStack.add(num);
while (notComplete) {
if (icp(ch) > isp(charStack.pop())) {
//如果要进栈的操作符优先级高
//则将数字进栈,运算符进栈
charStack.add(ch);
notComplete = false;//没有完成压栈操作?no,完成了
} else if (icp(ch) < isp(charStack.pop())) {
//如果要进栈操作符优先级低,则从栈中取出一个操作符和两个操作数进行计算
//将结果压入数字栈,然后接着判断(不需要让notComplete=false),直到该操作符进栈
double result = calculate();
doubleStack.add(result);
} else {
//当要进栈的操作符优先级和栈顶元素优先级相等,
//则只可能是栈顶元素是左括号,要进栈元素是右括号
if (i == length - 1) {
//如果此时右括号刚好是表达式最后一个字符,则只要将左括号移除
charStack.remove();
notComplete=false;
} else {
//如果不是在表达式末尾,需要提前对右括号后面的操作符进行处理,
//然后i++跳过后面的操作符
//这样做的目的是防止后面的操作符使数字栈多存入一个0
charStack.remove();
char operator = question.charAt(++i);
boolean flag = true;//设置一个临时变量判断有没有将右括号后面的操作符入栈
while (flag) {
if (icp(operator) > isp(charStack.pop())) {
//如果该操作符优先级比较高,则直接入栈即可,操作结束
charStack.add(operator);
flag = false;
} else {
//如果该操作符优先级比较低,则取出一个操作符,两个操作数进行运算,
//然后接着判断(先不让flag=false),直到该操作符进栈
double result = calculate();
doubleStack.add(result);
}
}
notComplete=false;
}
}
}
}
}
//整个算式已经遍历完,但是最后一个数字可能还没进栈,且字符栈内可能还有运算符,也就是说还没有运算结束
//如果最后一个字符是右括号,那么所有的数字都已经进栈了,
// 不需要把最后的number="0"压入栈内,否则需要压入栈内
if(question.charAt(length-1)!=')'){
System.out.println("遍历后将数字"+Double.parseDouble(number)+"压入栈内");
doubleStack.add(Double.parseDouble(number));
}
System.out.println("完成遍历");
while (!charStack.isEmpty()) {
double result=calculate();
doubleStack.add(result);
}
}catch( Exception e){
return "错误";
}
double answer = doubleStack.remove();
return""+answer;
}
public double calculate() throws Exception{
char operator=charStack.remove();
if(operator=='^'||operator=='+'||operator=='-'||operator=='*'||operator=='/'){
//双目运算
double num1=doubleStack.remove();
double num2=doubleStack.remove();
switch (operator){
case '^':
System.out.println("执行了一次指数运算: "+num2+"^"+num1);
return Math.pow(num2,num1);
case '+':
System.out.println("执行了一次加法:"+num2+"+"+num1);
return num2+num1;
case '-':
System.out.println("执行了一次减法:"+num2+"-"+num1);
return num2-num1;
case '*':
System.out.println("执行了一次乘法:"+num2+"*"+num1);
return num2*num1;
case '/':
if (num1==0){
throw new Exception();
}
System.out.println("执行了一次除法:"+num2+"/"+num1);
return num2/num1;
default:
System.out.println("单目操作符非法");
return 0;
}
}else {
//单目运算
double num=doubleStack.remove();
switch (operator){
case 's':
System.out.println("执行一次求正弦操作:sin"+num);
return Math.sin(num);
case 'c':
System.out.println("执行一次求余弦操作:cos"+num);
return Math.cos(num);
case 't':
System.out.println("执行一次求正切操作:tan"+num);
return Math.tan(num);
case 'a':
System.out.println("执行一次求绝对值操作:abs"+num);
return Math.abs(num);
case 'i':
System.out.println("执行一次求角度操作:arcsin"+num);
return Math.asin(num);
case 'o':
System.out.println("执行一次求角度操作:arccos"+num);
return Math.acos(num);
case 'n':
System.out.println("执行一次求角度操作:arctan"+num);
return Math.atan(num);
case 'q':
if (num<0){
throw new Exception();
}
System.out.println("执行一次开方操作:sqrt"+num);
return Math.sqrt(num);
default:
return 0;
}
}
}
public int isp(char ch) {//判断站内操作符优先级
switch (ch) {
case '#':
return 0;
case '(':
return 1;
case '+':
case '-':
return 3;
case '*':
case '/':
return 5;
case '^':
case 's':
case 'c':
case 't':
case 'i':
case 'o':
case 'n':
case 'a':
case 'q':
return 7;
case ')':
return 8;
default:
return -1;
}
}
public int icp(char ch) {//判断要进站操作符优先级
switch (ch) {
case '#':
return 0;
case ')':
return 1;
case '+':
case '-':
return 2;
case '*':
case '/':
return 4;
case '^':
case 's':
case 'c':
case 't':
case 'i':
case 'o':
case 'n':
case 'a':
case 'q':
return 6;
default:
return -1;
}
}
}
# 贷款计算器
该计算器比较简单,只要知道公式,由HTML DOM操纵标签内容即可,不做过多解释,关键代码如下:
<script>
var method = "";
function setMethod(obj) {
method =""+ obj.value;
}
function calculate() {
var principle = parseFloat(document.getElementById("principle").value);
var months = parseFloat(document.getElementById("months").value);
var rate = parseFloat(document.getElementById("rate").value);
var repayment = document.getElementById("repayment");
var interest = document.getElementById("interest");
var totalRepayment = document.getElementById("totalRepayment");
var totalInterest = document.getElementById("totalInterest");
try{
//数据预处理
principle *= 10000;
months *= 12;
rate *= 0.01;
if (method == "1") {
// alert("等额本息");
// alert("本金="+principle+"; 月数="+months+"; 利率="+rate);
//月均还款
var rep = (principle * rate * Math.pow(1 + rate, months)) / (Math.pow(1 + rate, months)-1);
repayment.innerHTML = rep.toFixed(2).toString();
//月均利息
var str = "";
var y=0;
for (var i = 1; i <=months; i++) {
y= principle * rate * (Math.pow(1 + rate, months) - Math.pow(1 + rate, i-1)) / (Math.pow(1 + rate, months)-1);
str += y.toFixed(2).toString() +",";
}
interest.innerHTML = str;
//还款总额
totalRepayment.innerHTML = (months * rep).toString();
//总利息
totalInterest.innerHTML=(months * rep-principle).toString();
} else {
// alert("等额本金");
// alert("本金="+principle+"; 月数="+months+"; 利率="+rate);
//月均还款
var paid=0;//累计已还款金额
var str1="";//记录月均还款
var str2="";//记录月均利息
var x=0;//月均还款
var y=0;//月均利息
var z=0;//总利息
for (var i=0;i<months;i++){
x=(principle/months)+(principle-paid)*rate;
str1+=x.toFixed(2).toString()+",";
y=(principle-paid)*rate;
str2+=y.toFixed(2).toString()+",";
paid+=y;
}
z=(((principle/months)+(principle*rate)+(principle*(1+rate)/months))/2)*months-principle;
//月均还款
repayment.innerHTML=str1;
//月均利息
interest.innerHTML=str2;
//还款总额
totalRepayment.innerHTML=paid.toFixed(2).toString();
//总利息
totalInterest.innerHTML=z;
}
}catch (e) {
repayment.innerHTML = "错误";
interest.innerHTML="错误";
totalInterest.innerHTML = "错误";
totalRepayment.innerHTML = "错误";
}
}
# 进制计算器
该计算器要实现二、八、十、十六进制的相互转化,主要思路是,先写一套十进制数转二、八、十六进制的算法,然后再写一套二、八、十六转十进制的算法,这样如果二转八可以先由二进制转十进制,然后由十进制转八进制。关键代码如下:
<script>
var num1="10";
var num2="10";
function toBinary(a) {//二进制
var bin="";
a=parseInt(a);
while(a!=0){
bin=(a%2).toFixed()+bin;
a=parseInt(a/2);
}
// alert(bin);
return bin;
}
function toHex(a){//十六进制
var hex="";
a=parseInt(a);
while(a!=0){
if (a%16<10){
hex=(a%16).toFixed()+hex;
}else if(a%16==10){
hex="A"+hex;
}else if (a%16==11){
hex="B"+hex;
}else if(a%16==12){
hex="C"+hex;
}else if (a%16==13){
hex="D"+hex;
}else if(a%16==14){
hex="E"+hex;
}else {
hex="F"+hex;
}
a=parseInt(a/16);
}
// alert(hex);
return hex;
}
function toOct(a) {//转为八进制
var oct="";
a=parseInt(a);
while(a!=0){
oct=(a%8).toFixed()+oct;
a=parseInt(a/8);
}
// alert(oct);
return oct;
}
function binaryToDec(str) {//二进制转为十进制
str=str.toString();
var n=1;
var len=str.length;
var dec=0;
for(let i=len-1;i>=0;i--){
var c=""+str.charAt(i);
dec=Number(c)*n+dec;
n*=2;
}
// alert(dec);
return dec;
}
function octToDec(str) {//八进制转十进制
str=str.toString();
var n=1;
var len=str.length;
var dec=0;
for(let i=len-1;i>=0;i--){
var c=""+str.charAt(i);
dec=Number(c)*n+dec;
n*=8;
}
// alert(dec);
return dec;
}
function hexToDec(str){//十六进制转十进制
str=str.toString();
var n=1;
var len=str.length;
var dec=0;
var ch='';
for(let i=len-1;i>=0;i--){
ch=str.charAt(i);
if (ch>='0'&&ch<='9'){
ch=""+ch;
dec=Number(ch)*n+dec;
n*=16;
}else if(ch=='A'){
dec=10*n+dec;
n*=16;
}else if(ch=='B'){
dec=11*n+dec;
n*=16;
}else if(ch=='C'){
dec=12*n+dec;
n*=16;
}else if(ch=='D'){
dec=13*n+dec;
n*=16;
}else if(ch=='E'){
dec=14*n+dec;
n*=16;
}else {
dec=15*n+dec;
n*=16;
}
}
// alert(dec);
return dec;
}
function setNum1(obj) {
num1=""+obj.value;
}
function setNum2(obj) {
num2=""+obj.value;
}
function convert() {
var dec="";
var inText=""+document.getElementById("inText").value;
var result=document.getElementById("result");
if(num1=="2"){
dec=""+binaryToDec(inText);
}else if (num1=="8"){
dec=""+octToDec(inText);
}else if(num1=="16"){
dec=""+hexToDec(inText);
}else {
dec=""+inText;
}
if (num2=="2"){
result.innerHTML=toBinary(dec);
}else if (num2=="8"){
result.innerHTML=toOct(dec);
}else if(num2=="16"){
result.innerHTML=toHex(dec);
}else {
result.innerHTML=dec;
}
}
</script>
OK,整个项目讲完了,是不是感觉人要没了?可以去访问我的GitHub源码,里面都有详细的注释,传送门: https://github.com/Nown1/calculator (opens new window)