項目簡介
本項目目的是寫一個計算器項目,主要考察算法(棧、後綴表達式)和基本的HTML DOM,JavaScript的應用。本項目的基本架構是SpringBoot+Thymeleaf+Ajax。本項目由五個計算器組成:第一個是商務計算器,主要實現加減乘除的連續運算,沒有優先級,給出第一個操作數,按下操作符,清空輸入欄,然後輸入第二個操作數,按下等於會顯示結果;第二個是四則計算器,涉及到帶有優先級的運算,能夠實現一個帶括號的四則表達式的運算;第三個是科學計算器,就是在四則計算器的基礎上添加了三角、反三角、指數運算;第四個是貸款計算器,可以由兩種方式(等額本金和等額本息,具體計算公式請百度)計算每月還款等數據;第五個是進制轉換計算器,可以實現二進制、八進制、十進制、十六進制之間的相互轉化。
以下內容僅包括算法解析。代碼中已經添加了足夠的註釋,具體項目源碼請訪問GitHub:
https://github.com/Nown1/calculator
商務計算器
商務計算器(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" οnclick="clear()">
<input type="button" value="DEL" οnclick="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