Clean & Blue 자세히보기

전공/전공 마스터리

자바 Swing으로 계산기 만들기 / 스택, 후위표기식, 사칙연산

_청렴 2022. 9. 27. 22:30
반응형

계산기 프로그램

계산기 프로그램을 만들기 위해서 세개의 클래스로 구성했다.

GUI화면 구성과 각 액션 리스너들을 달기 위한 CalMain 클래스와 실제 계산하는 기능을 할 calCore 클래스, CalMain의 텍스트 필드에서 값을 받아와 연산자 기준으로 숫자를 분리해줄 Tokenizer 클래스로 구성했다.

 

 

- Tokenizer 클래스

public class Tokenizer
{
	StringTokenizer token;
	String [] array;

	public Tokenizer(String text) 
	{
		token = new StringTokenizer(text, "+-*/()", true); 
		array = new String[token.countTokens()];
	}
	
	String [] tokenizing()
	{
		int i = 0;
		
		while(this.token.hasMoreTokens())
		{
			array[i] = this.token.nextToken();
			i++;
		}
		
		return array;
	}
}

Tokenizer 클래스에서는 calMain 클래스의 텍스트 필드에 작성된 수식들을 받아와서 연산자 기준으로 숫자들을 분리하여 문자열배열에 넣어주는 작업을 해줄 클래스이다.

calMain의 텍스트 필드에서 getText()하게되면 String으로 넘어오기 때문에 이를 문자열 배열로 만들어주기 위함이다.

 

Tokenizer() 생성자에는 token을 수식의 연산자들(+.-./.*,(,))을 지정해주고 9번라인에서는 만들어진 토큰의 갯수만큼 문자열 배열을 선언한다.

 

12번 라인의 tokenizing() 메소드는 실제 분리된 토큰들(분리된 숫자들과 연산자들)을 위에 만들어진 array에 하나씩 저장한다음 배열 주소를 return 한다.

 

 

- calCore 클래스

수식들을 문자열 배열로 받아와서 실제로 계산을 하는 클래스이다.

수식을 후위표기식으로 변환하고 계산하고 제곱하는 등의 기능을 넣었다.\

public class calcCore
{
	Stack<String> stack = new Stack<>(); // 사칙연산을 수행하기 위한 스택 준비
	ArrayList<String> s_Array = new ArrayList<String>();
    
    public static boolean isOperation(String str)
	{
		if(str.equals("+") || str.equals("-") || str.equals("/") || str.equals("*")) //문자열 객체는 equals 함수를 써야 일반 문자열과 비교가 가능함
			return true;
		else
			return false;
	}
    
    public static int priority(String str) // 연산자들의 우선순위를 비교하기 위한 메소드
	{
		if(str.equals("*") || str.equals("/"))
			return 2;
		else if(str.equals("+") || str.equals("-"))
			return 1;
		else if(str.equals("(") || str.equals(")")) // 괄호는 스택에서 연산자 비교 이후 꺼내지면 안되기 때문에 제일 낮은 우선순위를 준다.
			return 0;
		else 
			return -1;
	}

 

후위표기식을 만들기위한 Stack 컬렉션을 선언하고 Stack에서 pop하여 담을 ArrayList 컬렉션도 선언한다.

s_Array에 최종적으로 후위표기식이로 변환된 수식이 저장된다.

 

6번 라인 isOperation() 메소드를 이용해 토큰들이 들어올때 피연산자(숫자)인지 연산자인지 구분하도록 한다.

14번 라인에서는 prority() 메소드를 이용하여 각 연산자들의 우선순위를 정해준다.

 

public String [] postfix(String [] array) // 중위표기식을 후위표기식으로 바꾸는 메소드 정의
	{
		String [] str;
		
		for(int i=0; i<array.length; i++)
		{
			String now = array[i];
			
			switch(now)
			{
			case "*":
			case "/":
			case "+":
			case "-":
				while ( !stack.isEmpty() && (priority(stack.peek()) >= priority(now))) {
					s_Array.add(stack.pop());
				} // 연산자일 경우 스택의 피크와 우선순위를 비교한 후 우선순위가 피크가 더 높으면 pop하여 출력.
				stack.push(now); // 그 후 우선순위가 낮은 연산자를 push함
				break;
			case "(":
				stack.push(now); // 괄호 열기는 무조건 스택에 넣는다.
				break;
			case ")":
				while( !stack.isEmpty() && !(stack.peek().equals("("))) { // 문자열 객체는 equals 함수를 써야 일반 문자열과 비교가 가능함
					s_Array.add(stack.pop());
				} // 괄호 닫기는 괄호 열기를 만날때까지 스택에 있는 연산자들을 pop하여 출력함
					stack.pop(); // 그 이후 괄호 열기는 pop하여 버림
					break;
			default:
				s_Array.add(now); // 피연산자는 무조건 출력
			}
		}
	
		while(!stack.isEmpty())
			s_Array.add(stack.pop()); // 나머지 스택에 있는 요소들을 전부 출력
		
		str = s_Array.toArray(new String[s_Array.size()]);
		stack.removeAll(stack);
		s_Array.removeAll(s_Array);
		return str;
	}

토큰들(숫자와 연산자들)로 구성된 array 배열을 후위표기식으로 변환하는 메소드 postfix() 메소드를 작성한다.

 

5번라인, 토큰의 개수만큼 for문 반복한다.

now에 토큰을 가져온다.

9번 라인, 가져온 토크에 대하여 switch문을 돌린다.

15번 라인, 연산자일 경우 현재 스택의 피크와 now의 우선순위를 비교한다. 스택의 피크의 우선순위가 더 높거나 같을 경우 피크의 우선순위가 작아질때까지 pop하여 s_Array에 출력한다.

그 후 우선순위가 낮은 연산자는 스택에 push한다.

 

괄호열기는 무조건 스택에 넣고 괄호닫기는 괄호열기를 만날때까지 스택에서 계속 꺼내 s_Array에 출력한다. 이후 괄호열기는 그냥 pop하여 버린다.

그 외에 피연산자들은 무조건 s_Array에 출력한다.

 

출력된 s_Array 컬렉션을 배열 str로 변환 후 반환한다.

 

public double calculating(String [] array) // 스택을 이용하여 실제 사칙연산 계산을 수행하는 메소드
	{
		double result = 0;
		double front, rear;
		
		for(int i=0; i<array.length; i++)
		{
			String now = array[i];
			
			if(!isOperation(now))
				stack.push(now); // 피연산자라면 스택에 넣는다.
			else
			{
				rear = Double.parseDouble(stack.pop()); // 연산자라면 피연산자 두개를 스택에서 뽑아낸다.
				front = Double.parseDouble(stack.pop());
				
				switch(now)
				{
				case "+": result = front + rear;
				break;
				case "-": result = front - rear;
				break;
				case "*": result = front * rear;
				break;
				case "/": result = front / rear;
				break;
				}
				stack.push(Double.toString(result)); // 계산된 피연산자를 다시 스택에 넣는다.
			}
		}
		
		result = Double.parseDouble(stack.pop());
		stack.removeAll(stack);
		s_Array.removeAll(s_Array);
		return result;
	}

후위표기식으로 완성된 array를 이용해 실제 사칙연산을 수행하는 메소드 calculating() 메소드를 작성한다.

10번 라인, array에서 뽑아낸 값이 연산자인지 피연산자인지 구별하고 피연산자일 경우 스택에 push한다.

14번 라인, 연산자를 만날 경우 스택에서 pop을 두번 하여 각각 rear와 front 변수에 넣는다.

그 이후 연산자 종류에 따라 rear와 front를 계산하고 계산된 값을 다시 스택에 push한다.

 

최종값을 스택에서 pop하여 꺼내 반환한다.

 

public double Square(String str) // 제곱을 위한 메소드
	{	
		double result;
	
		if(str.contains("+") || str.contains("-") || str.contains("/") || str.contains("*") || str.contains("(") || str.contains(")"))
		{
			JOptionPane.showMessageDialog(null, "연산자가 없는 값을 입력하세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			return 0;
		}
		else if(str.isBlank())
		{
			JOptionPane.showMessageDialog(null, "값을 입력하세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			return 0;
		}
		else
		{
			result = Double.parseDouble(str)*Double.parseDouble(str);
			return result;
		}
	}
	
	public double Percentage(String str) //퍼센테이지를 위한 메소드
	{
		double result;
		
		if(str.contains("+") || str.contains("-") || str.contains("/") || str.contains("*") || str.contains("(") || str.contains(")"))
		{
			JOptionPane.showMessageDialog(null, "연산자가 없는 값을 입력하세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			return 0;
		}
		else if(str.isBlank())
		{
			JOptionPane.showMessageDialog(null, "값을 입력하세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			return 0;
		}
		else
		{
			result = Double.parseDouble(str)*100;
			return result;
		}
	}
			
}

제곱계산을 위한 메소드와 백분율 계산을 위한 메소드를 만든다.

 

 

- CalMain 클래스

public class CalMain extends JFrame
{
	
	Container contentPane;
	
	String parameter = new String();
	JTextField textfield;
	Tokenizer tk;
	calcCore core = new calcCore();
	
	public CalMain()
	{
		setTitle("Swing GUI 계산기");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		contentPane = getContentPane();
		contentPane.setLayout(new BorderLayout());
		
		textfield = new JTextField();
	    textfield.setFont(new Font("고딕체", Font.ITALIC, 45)); 
		// JTextFiled의 높이를 조정하고 싶으면 폰트 크기를 키우면 된다.
	    textfield.addKeyListener(new KeyAdapter()
		{
			public void keyPressed(KeyEvent e)
			{
				if(e.getKeyCode() == KeyEvent.VK_ENTER)
				{
					tk = new Tokenizer(textfield.getText());
					String [] array = tk.tokenizing();
					array = core.postfix(array);
					double result = core.calculating(array);
					
					textfield.setText(Double.toString(result));	
				}
			}
		});
		
		contentPane.add(textfield, BorderLayout.NORTH);
		
		Container middlePane = new Container();
		contentPane.add(middlePane, BorderLayout.CENTER);
		
		middlePane.setLayout(new GridLayout(6, 4));
		
		JButton [] Jb = new JButton[24];		
		for(int i=0; i<Jb.length; i++)
		{
			Jb[i] = new JButton();	
			Jb[i].setFont(new Font("고딕체", Font.ITALIC, 18));
		}
		for(int i=0; i<Jb.length; i++)
			middlePane.add(Jb[i]);
		
		Jb[0].setText("%");
		Jb[0].addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e)
			{
				double result = core.Percentage(textfield.getText());
				textfield.setText(Double.toString(result)+"%");
				textfield.requestFocus();
			}
		});
		Jb[1].setText("CE");
		Jb[1].addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e)
			{
				try {
					textfield.setText("");
					parameter = "";
				} catch(StringIndexOutOfBoundsException e1) {
					textfield.setText(parameter);
				}
				textfield.requestFocus();
			}
		});
		Jb[2].setText("C");
		Jb[2].addMouseListener(new MouseAdapter() {
			
			public void mousePressed(MouseEvent e)
			{
				try {
					textfield.setText("");
					parameter = "";
				} catch(StringIndexOutOfBoundsException e1) {
					textfield.setText(parameter);
				}
				textfield.requestFocus();
			}
		});
		Jb[3].setText("<<");
		Jb[3].addMouseListener(new MouseAdapter() {
		
			public void mousePressed(MouseEvent e)
			{
				try {
					parameter = parameter.substring(0, parameter.length()-1); // 시작위치부터 마지막에서 하나를 뺀 위치까지의 문자열을 잘라낸다.
					textfield.setText(parameter);
				} catch(StringIndexOutOfBoundsException e1) {
					textfield.setText(parameter);
				}
				textfield.requestFocus();
			}
		});
		
		Jb[4].setText("(");
		Jb[4].addMouseListener(new NumberListener());
		Jb[5].setText(")");
		Jb[5].addMouseListener(new NumberListener());
		
		Jb[6].setText("^");
		Jb[6].addMouseListener(new MouseAdapter(){
			public void mousePressed(MouseEvent e) 
			{
			double result = core.Square(textfield.getText());
			
			textfield.setText(Double.toString(result));
			textfield.requestFocus();
			}
		});
		
		Jb[7].setText("÷");
		Jb[7].addMouseListener(new NumberListener());
		Jb[11].setText("×");
		Jb[11].addMouseListener(new NumberListener());
		Jb[15].setText("-");
		Jb[15].addMouseListener(new NumberListener());
		Jb[19].setText("+");
		Jb[19].addMouseListener(new NumberListener());
		
		Jb[23].setText("=");	
		Jb[23].addMouseListener(new EqualListener());
		
		Jb[8].setText("7");
		Jb[8].addMouseListener(new NumberListener());
		Jb[9].setText("8");
		Jb[9].addMouseListener(new NumberListener());
		Jb[10].setText("9");
		Jb[10].addMouseListener(new NumberListener());
		Jb[12].setText("4");
		Jb[12].addMouseListener(new NumberListener());
		Jb[13].setText("5");
		Jb[13].addMouseListener(new NumberListener());
		Jb[14].setText("6");
		Jb[14].addMouseListener(new NumberListener());
		Jb[16].setText("1");
		Jb[16].addMouseListener(new NumberListener());
		Jb[17].setText("2");
		Jb[17].addMouseListener(new NumberListener());
		Jb[18].setText("3");
		Jb[18].addMouseListener(new NumberListener());
		Jb[21].setText("0");
		Jb[21].addMouseListener(new NumberListener());
		
		Jb[22].setText(".");
		Jb[22].addMouseListener(new NumberListener());
		Jb[20].setText("±");
		Jb[20].addMouseListener(new SignChange());
		
		setSize(400, 500);
		setVisible(true);
		
	
	
	}
	
	public class NumberListener extends MouseAdapter
	{
		public void mousePressed(MouseEvent e)
		{
			JButton tmp = (JButton)e.getSource();
			
			if(tmp.getText().equals("×")) {
				parameter = parameter.concat("*");
				textfield.setText(parameter);
			}
			else if(tmp.getText().equals("÷")) {
				parameter = parameter.concat("/");
				textfield.setText(parameter);
			}
			else {
				parameter = parameter.concat(tmp.getText());
				textfield.setText(parameter);
			}
			textfield.requestFocus();
		}
	}
	
	public class EqualListener extends MouseAdapter
	{
		public void mousePressed(MouseEvent e)
		{
			tk = new Tokenizer(textfield.getText());
			String [] array = tk.tokenizing();
			array = core.postfix(array);
			double result = core.calculating(array);
			
			textfield.setText(Double.toString(result));	
			textfield.requestFocus();
		}
	}
	
	public class SignChange extends MouseAdapter
	{
		public void mousePressed(MouseEvent e)
		{
			String str = textfield.getText();
			
			if(str.contains("+") || str.contains("/") || str.contains("*") || str.contains("(") || str.contains(")"))
			{
				JOptionPane.showMessageDialog(null, "연산자가 없는 값을 입력하세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			}
			else if(str.isBlank())
			{
				JOptionPane.showMessageDialog(null, "값을 입력하세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			}
			else if(str.contains("-"))
			{
				String [] string = str.split("-");
				if(string.length == 2)
				{
					if(!string[0].isBlank())
						JOptionPane.showMessageDialog(null, "수식을 입력하지 마세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
					else
						textfield.setText(string[1]);
				}
				else
					JOptionPane.showMessageDialog(null, "수식을 입력하지 마세요!", "Warning!", JOptionPane.ERROR_MESSAGE);
			}
			else if(str.equals("0") || str.equals("0.0"))
			{
				textfield.setText("0");
			}
			else
			{
				textfield.setText("-"+textfield.getText());
			}
			textfield.requestFocus();
		}
	}
	
	
	public static void main(String[] args)
	{
		new CalMain();
	}

}

GUI화면을 구성하고 각 버튼에 리스너들을 달아중 후 main 메소드에서 실행한다.