Scroll to top

Basic Calculator Tutorial with React Native (No Expo)


Cesar Rodriguez Chavez - October 1, 2020 - 0 comments

This is what we are going to require in terms of React and React Native

React Native + .63

React 16+

If you haven’t worked with RN before, I suggest you follow the guide below to get you up to speed. Remember to do it with react-native cli since we are not using expo this time. Also make sure you select the right OS.

https://reactnative.dev/docs/environment-setup

Here’s what you’ll need:

  • node 8+
  • [Homebrew](https://brew.sh/)
  • [Watchman](https://facebook.github.io/watchman)
  • Xcode
  • Android Studio
  • CocoaPods

If you need any further help or a closer look into this, the repo for this implementation is available here: https://github.com/c3sarr0driguez/rn_basic_calculator

Let’s get started.

npx react-native init {ProjectName} && cd {ProjectName}

Example:

npx react-native init BasicCalculator && cd BasicCalculator

After this, we are going to follow best practices and start building. First, we need a folder structure like this:

This is what we are going to build today.

![Calculator](appSample.png “Calculator”)

## Folder Structure

src	|
	|_ assets
	|_ components
		|_text...
		|_calcButton...
	|_ pages
	|		|_main
	|_ utils
	|		|_constants
	|		|_styles
	|_contexts

<strong> Since this is a technical tutorial, I won’t spend time explaining styling. But, I will include them in this exercise. </strong>

We are going to use yarn to install dependencies

First we are going to integrate everything speaking about developer environment

To have a good linter we need to install

yarn add --dev eslint prettier @react-native-community/eslint-config

And then add to your eslint config (`.eslintrc`, or `eslintConfig` field in `package.json`):

{
	"extends":  "@react-native-community",
	"settings":  {
		"import/resolver":  {
		"babel-module":  {}
		}
	},
}

this will install prettier and eslint configurations so you can be now able to detect issues and code styling issues in your code , for this step we are going to include then

{
 "scripts":  {
	 ...
	 "lint": "eslint ./src"
	}
}

After that we are going to install babel module resolver to make absolute imports in our project

yarn add –dev eslint-plugin-import eslint-import-resolver-babel-module

babel-plugin-module-resolver

and compliment your `.babelrc`or `babel.config.js` with

{
  "plugins": [
    ["module-resolver", {
      "root": ["./src"],
    }]
  ]
}

Finally for the development you will add the proper config for our editor to visualize the imports correctly by adding a file ‘jsconfig.json’ With the following content

{
	"compilerOptions": {
		"baseUrl": ".",
		"paths": {
			"*": ["src/*"]
		}
	}
}

Before we start, we are going to install the libs we missed

responsive  'yarn add react-native-responsive-screen'

# Hands on

We are going to first add a screen to pages under the name of main.js and the style file main.styles.js with empty content

pages
	|__main
			|__main.js
			|__main.styles.js
```

### App.js

import  React  from  'react';
import  Main  from  'screens/main/main';

function  App() {

	return (
		<Main/>
	);
}

export  default  App;

### Components
We are going to start developing our shared(universal components) which are text calcButton and mainButton

### Important!

All components we are going to build will have more or less the very same structure. If our styles are complex to add, we need to abstract that separation of concern into a separate function. For example, let’s say that you have a button component that you need to have different behaviors through properties:

const SIZE = {
	SM: 'sm',
	MD: 'md',
}

const STYLE = {
	BOLD: 'bold',
	ITALIC: 'italic',
	UPPERCASE: 'uppercase',
}

const VARIANT = {
	PRIMARY: 'primary',
	SECONDARY: 'secondary',
	SOMETHING: 'something',
}

This is how we should abstract all the logic. Otherwise, the button will be hard to read. So, this hypothetic function should deliver all the styles compounded. If you have a complex component with subcomponents that can be themselves customized, then you should not deliver just one compounded object but several like

componentStyles = {
         text: textStyles,
         button: buttonStyles
}

return componentStyles

Then assign this to the right components

Let’s take the following function

function  mapStateAndPropsToStyles({color, textColor, style}) {
	const  componentStyles = [styles.calcButton];
	if (color) {
	componentStyles.push({backgroundColor:  color});
	}
		if (textColor) {
	componentStyles.push({color:  textColor});
	}
	if (style) {
		if (Array.isArray(style)) {
			componentStyles.push(...style);
		} else {
			componentStyles.push(style);
			}
	}

	return  componentStyles;
}

So, as we can see, we start assigning  the baseStyles.
`const componentStyles = [styles.calcButton];`

If you are going to have more stuff like the variants we mentioned earlier, you can make use of optionalParameters. With this, you can set  a default value and have assurance that your style will have  all the properties you want (may not be explicit)

Let’s say

function mapStateAndPropsToStyle({variant=VARIANT.PRIMARY, size=SIZE.MD}) {
...
}

Or, you can use defaultProps to do the  same thing.

So, remember that all of our components will use the same technique.

Finally, all of our components may or may not have a drill props function in case we are aliasing or controlling how children components behave. Let’s say that under our button we have a text that has its own properties like  size and color variants because we have 3 different combination of variables that we do not want our users to experiment with, at least not freely due to style concerns or requirements. Then we are going to have another abstraction of those  to decorator that ask for a property.

Let’s say

const TEXT_VARIANT = {
	PRIMARY: 'FIRST',
	SECONDARY: 'SECONDARY'
}

Let’s suppose now that the first variant will have
* size sm
* variant bold
* color white

You see the problem?
This is why we are going to map our state/props to text props, to control the text children

function  drillProps({variant}) {
	if(variant === VARIANTS.PRIMARY) {
		return {
			color:  'red',
			size: 'md'
		};
	} else if() {
		return {
			color:  'blue',
			size: 'sm'
		};
	} else {
		throw TypeError(`Not implemented variant ${variant}`
	}

}

So, let’s recap.

Since  the structure of all of these components is the very same, we are not going to explain any more about these capabilities.
We have a function to translate state and props to complex styles that could return one object for the root component, and if needed more to be mapped to several components
‘function mapStateAndPropsToStyles’
The function to decorate other component’s through an interface that will take control over the children elements but will expose certain functionalities to change the content like inner text
‘function drillDownStateAndProps’
Do not get stuck with the names,  just be consistent.

###CalcButton.js

function  CalcButton({
	color,
	textColor,
	onPress,
	children,
	textProps = {},
	style,
}) {

return (
		<TouchableOpacity
			onPress={onPress}
			style={mapStateAndPropsToStyles({
			color,
			textColor,
			style,
		})}>
		<Text  {...drillsDownStateAndprops({textColor})}  {...textProps}>

		{children}
		</Text>
		</TouchableOpacity>
	);
}

Simple right?
We are making use of ‘drillsDownStateAndprops’ and ‘mapStateAndPropsToStyles’ as we discussed. So, this is the complete code.

// libs

import  React, {useContext, memo} from  'react';
import  PropTypes  from  'prop-types';
import {TouchableOpacity} from  'react-native';
import  Text  from  'components/text';
import  ThemeContext  from  'contexts/themes';

// styles
import  styles  from  './calcButton.styles';

function  mapStateAndPropsToStyles({color, textColor, style}) {

	const  componentStyles = [styles.calcButton];
	if (color) {
		componentStyles.push({backgroundColor:  color});
	}
	if (textColor) {
		componentStyles.push({color:  textColor});
	}

	if (style) {
		if (Array.isArray(style)) {
		componentStyles.push(...style);
		} else {
		componentStyles.push(style);
		}
	}
	return  componentStyles;
}

function  drillDownStateAndProps({textColor}) {
	return {
		color:  textColor,
	};
}



function  CalcButton(color,
textColor,
onPress,
children,
textProps = {},
style) {
	return (
			<TouchableOpacity
			onPress={onPress}
			style={mapStateAndPropsToStyles({
			color,
			textColor,
			style,
			})}>
			<Text  {...drillDownStateAndProps({textColor})}  {...textProps}>
				{children}
			</Text>
		</TouchableOpacity>
		);

}

// We make use of proptypes to provide a strong component signature
CalcButton.propTypes = {
	onPress:  PropTypes.func,
	textColor:  PropTypes.string,
	textProps:  PropTypes.object,
	color:  PropTypes.string,
	style:  PropTypes.oneOfType([
	PropTypes.number,
	PropTypes.object,
	PropTypes.arrayOf(PropTypes.number),
	PropTypes.arrayOf(PropTypes.object),
	]),
};

export  default  memo(CalcButton);

### CalcButton.styles.js

import {StyleSheet} from  'react-native';
import {COLOR} from  'utils/styles';
const  baseStyles = {
	calcButton: {
		flex:  1,
		justifyContent:  'center',
		alignItems:  'center',
		color:  COLOR.BLACK,
	},
};

const  styles = StyleSheet.create(baseStyles);
export  default  styles;

Let´s move on with next component common text

Text.js

import  React  from  'react';
import  PropTypes  from  'prop-types';
import {Text, TextPropTypes} from  'react-native';
import {COLOR} from  'utils/styles';

import  styles  from  './text.styles';

function  mapStateAndPropsToStyle({size, color}) {
	const  componentStyles = [styles.text];
	if (size && styles[size]) {
	componentStyles.push(styles[size]);
	}
	if (color) {
	componentStyles.push({color});
	}
	return  componentStyles;
}

export  default  function  CalcText({size, color, children, ...rest}) {
return (
		<Text  {...rest}  style={mapStateAndPropsToStyle({size, color})}>
			{children}
		</Text>
	);
}


CalcText.propTypes = {
...TextPropTypes,
size:  PropTypes.string,
};

CalcText.defaultProps = {
size:  'md',
color:  COLOR.BLACK,
};

Let’s summarize. We again make use of  the function masStateAndPropsToStyle to calculate dynamic styles.
Besides that, everything else is self explanatory. One more thing we did is that we supplied defaultProps to have a default behavior in case the developer delivers a plain props object.

### Text.styles.js

import {StyleSheet} from  'react-native';
import {widthPercentageToDP  as  wp} from  'react-native-responsive-screen';

const  baseStyles = {
	text: {
		fontFamily:  'Audiowide-Regular',
	},
	xs: {
		fontSize:  wp(2),
	},
	sm: {
		fontSize:  wp(4),
	},
	md: {
		fontSize:  wp(6),
	},

	lg: {
	fontSize:  wp(8),
	},
};
const  styles = StyleSheet.create(baseStyles);
export  default  styles;

Finally, let’s start with mainButton.js

This is the very same as our normal button but, a gradient version of it, Since gradient has no support, by default we are going to install another dependency.

'yarn add react-native-linear-gradient

Please do not forget to follow this tutorial for a good installation  https://github.com/react-native-community/react-native-linear-gradient
For ‘iOS’, it is as simple as run ‘npx pod-install ios’ but for ‘android’ it is more complex. Remember that in this version of rn you are no longer required to add the pod line. rn will take care of it dynamically. However, if you are using a lower version please make sure you add the pod line in podfile.

Now,

### mainButton.js

import  React, {useContext} from  'react';
import {TouchableOpacity} from  'react-native';
import  PropTypes  from  'prop-types';
import  LinearGradient  from  'react-native-linear-gradient';
import  Text  from  'components/text';
import  ThemeContext  from  'contexts/themes';

import  styles  from  './mainButton.styles';

function  mapStateAndPropsToStyles({style, linearGradientStyle}) {
	const  componentStyles = {
		mainButton: [styles.mainButton],
		linearGradient: [styles.linearGradient],
	};
	if (style) {
		if (Array.isArray(style)) {
		componentStyles.mainButton.push(...style);
		} else {
			componentStyles.mainButton.push(style);
		}
	}
	if (linearGradientStyle) {
		if (Array.isArray(linearGradientStyle)) {
			componentStyles.linearGradient.push(...linearGradientStyle);
		} else {
			componentStyles.linearGradient.push(linearGradientStyle);
		}
	}

	return  componentStyles;
}

function  drillProps({textColor}) {
	return {
		color:  textColor,
	};
}

function  MainButton(props) {
	const [theme] = useContext(ThemeContext);
	const {mainButton: themeMainButtonProps = {}} = theme;
	const  mixedProps = {...themeMainButtonProps, ...props};
	const {
		colors,
		textColor,
		onPress,
		children,
		textProps = {},
		style,
	} = mixedProps;
	const {
	mainButton: mainButtonStyle,
	linearGradient: linearGradientStyle,
	} = mapStateAndPropsToStyles({style});

	return (
		<TouchableOpacity  style={mainButtonStyle}  onPress={onPress}>
			<LinearGradient  colors={colors}  style={linearGradientStyle}>
				<Text  {...drillProps({textColor})}  {...textProps}>
					{children}
				</Text>
			</LinearGradient>
		</TouchableOpacity>
	);
}

MainButton.propTypes = {
	onPress:  PropTypes.func,
	textColor:  PropTypes.string,
	textProps:  PropTypes.object,
	colors:  PropTypes.arrayOf(PropTypes.string),
	style:  PropTypes.oneOfType([
	PropTypes.number,
	PropTypes.object,
	PropTypes.arrayOf(PropTypes.number),
	PropTypes.arrayOf(PropTypes.object),
	]),
	linearGradientStyle:  PropTypes.oneOfType([
	PropTypes.number,
	PropTypes.object,
	PropTypes.arrayOf(PropTypes.number),
	PropTypes.arrayOf(PropTypes.object),
	]),
};

export  default  MainButton;

Nothing else to explain ver  implementation other than the components

### mainButton.styles.js

import {StyleSheet} from  'react-native';

const  baseStyles = {
	mainButton: {
		flex:  1,
	},
	linearGradient: {
		height:  '100%',
		width:  '100%',
		justifyContent:  'center',
		alignItems:  'center',
	},
};

const  styles = StyleSheet.create(baseStyles);
export  default  styles;

Now we are at the fun part. Let’s analyse how we are going to do the hard part. We are going to have a data structure like this:

{
left: '9',
op: '*',
right: '4324'
}

A binary structure, if this was another languages other than js we could add an operator overload to achieve what we want, since operators are binary operators like 9 * 8, or 7 / 2
So we are going to always have a left value, our initial would be 0. If the user clicks something that is a number, we will be adding
entries to that number, because it is zero. Since it has no value we will replace it for the first digit. Notice that  ‘.’counts as a new digit and cannot be set twice.

For example

If user presses 9, then 8,  then  ‘.’ ,and then 9 again.
We will be
* replacing 0 by 9
* adding 8
* adding .
* adding 9
* and ignoring ‘.’

<strong><em>However, we need to make sure we are writing on the right side.  Are on the left side or on the right side???</em></strong>

To answer this question is going to be simple. We only need to check if  the operator is set. We need to write on the right side or on the left operator.

So let’s start.
This is the react tree structure:

function  debug() {
	return  JSON.stringify({left, op, right}, null, '\t');
}

const {
	mainScreen: mainScreenStyles,
	lowerContainer: lowerContainerStyles,
	} = mapStateAndPropsToStyles({
	style,
	lowerContainerStyle,
});

<View  style={mainScreenStyles}>
	<View  style={styles.upperContainer}>
		{/* <View>
		<Text>{debug()}</Text>
		</View> */}
	</View>
	<Text  size="lg">{right || left}</Text>
	<View  style={lowerContainerStyles}>
		<View  style={styles.row}>
			<Button  onPress={() =>  restart(0)}>C</Button>
			<Button  onPress={() =>  handlePressSwitchSign()}>+/-</Button>
			<Button  onPress={() =>  handlePressPercentage()}>%</Button>
			<Button  onPress={() =>  handlePressOp('/')}>/</Button>
		</View>
		<View  style={styles.row}>
			<Button  onPress={() =>  handlePressDigit('7')}>7</Button>
			<Button  onPress={() =>  handlePressDigit('8')}>8</Button>
			<Button  onPress={() =>  handlePressDigit('9')}>9</Button>
			<Button  onPress={() =>  handlePressOp('*')}>x</Button>
		</View>
		<View  style={styles.row}>
			<Button  onPress={() =>  handlePressDigit('4')}>4</Button>
			<Button  onPress={() =>  handlePressDigit('5')}>5</Button>
			<Button  onPress={() =>  handlePressDigit('6')}>6</Button>
			<Button  onPress={() =>  handlePressOp('+')}>
			+
			</Button>
		</View>
		<View  style={styles.row}>
			<Button  onPress={() =>  handlePressDigit('1')}>1</Button>
			<Button  onPress={() =>  handlePressDigit('2')}>2</Button>
			<Button  onPress={() =>  handlePressDigit('3')}>3</Button>
			<Button  onPress={() =>  handlePressOp('-')}>
			-
			</Button>
		</View>
		<View  style={styles.row}>
			<Button  onPress={() =>  handlePressDigit('0')}>
			0
			</Button>
			<Button  onPress={() =>  handlePressDigit('.')}
			.
			</Button>
			<MainButton  style={styles.mainButton}  onPress={equal}>
			=
			</MainButton>
		</View>
	</View>
</View>

We can uncomment the debug piece so all the time you will be able to see the behavior of our state <strong><em>which  is skipped for brevity</em></strong>

So, this is what we will see at all times.

left: ‘0’,
op: null,
right: null,
lastOp: null,

Notice the <strong><em>lastOp</em></strong> function. This is important since we are going to use that to repeat operations if the user keeps pressing ‘=’

Let’s suppose the user clicks 5 then + then 5 then =, and from there =, =
That way the first operation performed + 5 will be added to the result every time.

### Important!!!

Since we are going to present all data in calculator to the user, we always need to convert our results to string. That way, any entry can be concatenated.

So, let’s start with the function of numbers and the dot:

### handlePressDigit Method

function  handlePressDigit(digit) {
	const  newState = {};
	if (op) {
		if (right === '0' && digit === '0') {
			return;
		}

		const  isDot = digit === '.';
		if (isDot && right && right.indexOf('.') !== -1) {
		return;
		}
		newState.right =
		right === '0' || !right ? (isDot ? '0.' : digit) : `${right}${digit}`;
	} else {
		if (left === '0' && digit === '0') {
		return;
		}
		if (digit === '.' && left.indexOf('.') !== -1) {
		return;
		}
		newState.left =
			left === '0' || !left ? (isDot ? '0.' : digit) : `${left}${digit}`;
	}


// mix between prevstate and new state
setState((prevState) => ({
	...prevState,
	...newState,
}));
}

We start with a new empty state, from where we are going to prepare. If left side is already taken, we know the op value otherwise it means that we are on the left side. If the digit is zero and we have zero or is a dot and we already have a dot we are going to escape. If not we have two courses of action:

if current side (left or right accordingly) is empty
	- then check if is dot
			- will be `0.` something
	else
		will replace the 0/null for the digit in question
else
	will add at the end the new digit `${left}${digit}`;

So far so good. We are almost done. The other important thing will be the calculations. This is more complex, so pay attention. We are close to end.

Lets pass to operators

### handlePressOp Method

function  handlePressOp(nextOp) {

	let  nextState = {};

	if (op && right) {
		const  result = calc(left, op, right);
		nextState = {
			right:  null,
			left:  result,
		};
	}


	setState((prevState) => ({
	...prevState,
	...nextState,
	op:  nextOp,
	}));
}

Operation can be one of  ‘*’ ‘/’ ‘+’ ‘-‘

If left op already exists, We will proceed to make the calculation between left , op and right, meaning the result.  For example:
9 * 9, otherwise it will only register the nextOp to perform, for example if you have
* left = 90 and no op
* then press `*`
* then press `+`

Nothing will happen , only we are going to change ‘*’  with ‘+’ as next operation. Remember that when op is set, you can start typing and whatever you start typing is going to be redirected to right, and if by chance you try ‘=’ or ‘*’ or any operator again <strong><em>you will end up doing the calculation, then empty right side and pass the result to left side, and so on and so forth</em></strong>

Let’s pass to calc method

### Calc Method

function  calc(left, op, right) {

	if (!left || !op || !right) {
	return;
	}
	const  maxDecimals =
	(left.split('.')[1]?.length || 0) + (right?.split('.')[1]?.length || 0);

	switch (op) {

		case  '*': {
			return  getRidOfRightZeroes(multiply(left, right, maxDecimals));
		}
		case  '/': {
			return  getRidOfRightZeroes(divide(left, right, maxDecimals));
		}
		case  '+': {
		return  getRidOfRightZeroes(add(left, right, maxDecimals));
		}
		case  '-': {
			return  C(substract(left, right, maxDecimals));
		}
		default:
			throw  new  Error(`${op} Not implemented`);
	}

}

First we are going to check if we have everything we need to perform a calc which is ‘left’ , ‘op’ and ‘right’.

After that, since we are managing decimal operations we need to make sure we retrieve the right number of decimals. For this we are going to emulate our computer’s calculator behavior. Just Add both decimal numbers, left and right.

If we have
* 98089.9 * 89.10
then our max number of decimals is 3

Then we are going to check if operation is registered. If not, we throw an error and finally just get rid of  the extra zeroes and perform the operation <strong><em>Remember what we discussed. All operations will convert to number perform operation’s and then get back to strings</em></strong>

Let’s say we have 59.09000
In that case we only care about 59.09 right?  That is what the getRidOfRightZeroes does. Let’s take a closer look .

### getRidOfRightZeroes Method

function  getRidOfRightZeroes(result) {
	const [, decimals] = result.split('.');
	if (!decimals) {
	return  result;
	}
	let  charsToRemove = 0;
	const  len = result.length - 1;
	for (let  i = len; i > -1; i--) {
		const  lastChar = result[i];
		if (lastChar === '0') {
			charsToRemove++;
		} else  if (lastChar === '.') {
			charsToRemove++;
			break;
		} else {
			break;
		}
	}
	if (!charsToRemove) {
	return  result;
	}
	return  result.slice(0, -charsToRemove);
}

We are going to look for decimals. If not then return the same result.

But, if  there are decimals, we sit at the right side and start moving to the left side until we hit a number other than 0 or .
If we have `89.00` then it will become `89`

Almost done. Let’s look at the operations implementation.

### Operations Methods

function  divide(left, right, decimals = 0) {
	if (left === '0' || right === '0') {
		return  '0';
	}
	return (+left / +right).toFixed(decimals);
}

function  multiply(left, right, decimals = 0) {
	if (left === '0' || right === '0') {
		return  '0';
	}
	return (+left * +right).toFixed(decimals);
}

function  add(left, right, decimals = 0) {
	return (+left + +right).toFixed(decimals);
}

function  substract(left, right, decimals = 0) {
	return (+left - +right).toFixed(decimals);
}

Just one thing to explain here: if we use the operator ‘+’ before any variable, it will try to convert to a number even when their is a negative number like ‘-89’ will be converted to  ‘-89’

Last but not least, let’s examine the equal functionality.

function  equal() {

let  nextState = null;

	if (op && right) {
		nextState = {
			left:  calc(left, op, right),
			right:  null,
			lastOp: {
				op,
				right,
			},
	};

	} else  if (lastOp) {
		const {op, right} = lastOp;
		nextState = {
			left:  calc(left, op, right),
			right:  null,
		};
	} else  if (op) {
		nextState = {
			left:  calc(left, op, left),
			right:  null,
			lastOp: {
				op,
				right:  left,
			},

		};
	}
	if (!nextState) {
	return;
	}

	setState((prevState) => ({
	...prevState,
	...nextState,
	}));
}
  • First we are creating a ref to nextState
  • If there is an op to perform, it is done by checking the presence of ‘right’ and ‘op’ same thing as operator handling. If those elements are present we proceed to calc and assign that to ‘left’ side and empty ‘right’ side
  • But if there is a ‘lastOp’ registered, we go with that one.
  •  If their are none but there is a left value and an ‘op’ then we finally go with duplicating the left value to the right value and calculating

Full code for Main.js

import  React, {useState, useEffect, useContext} from  'react';
import {View, TouchableOpacity} from  'react-native';
import  PropTypes  from  'prop-types';
import  Button  from  'components/calcButton';
import  MainButton  from  'components/mainButton';
import  Text  from  'components/text';
import {COLOR} from  'utils/styles';
import  LinearGradient  from  'react-native-linear-gradient';
import  ThemeContext  from  'contexts/themes';
import {THEME} from  'utils/themes';
import  styles  from  './main.styles';
const  INITIAL_STATE = {
history: [],
left:  '0',
op:  null,
right:  null,
lastOp:  null,
};
function  mapStateAndPropsToStyles({isDarkish, style, lowerContainerStyle}) {
const  componentStyles = {
lowerContainer: [styles.lowerContainer],
mainScreen: [styles.mainScreen],
};
if (isDarkish) {
componentStyles.lowerContainer.push(styles.darkish);
}
if (lowerContainerStyle) {
if (Array.isArray) {
componentStyles.lowerContainer.push(...style);
} else {
componentStyles.lowerContainer.push(style);
}
}
if (style) {
if (Array.isArray) {
componentStyles.mainScreen.push(...style);
} else {
componentStyles.mainScreen.push(style);
}
}
return  componentStyles;
}
function  MainScreen(props) {
const [theme, setTheme] = useContext(ThemeContext);
const {mainScreen: themeMainScreenProps = {}} = theme;
const [{left, op, right, history, lastOp}, setState] = useState(
INITIAL_STATE,
);
const  mixedProps = {...themeMainScreenProps, ...props};
const {isDarkish, style, lowerContainerStyle} = mixedProps;
function  calc(left, op, right) {
if (!left || !op || !right) {
return;
}
const  maxDecimals =
(left.split('.')[1]?.length || 0) + (right?.split('.')[1]?.length || 0);
switch (op) {
case  '*': {
return  getRidOfRightZeroes(multiply(left, right, maxDecimals));
}
case  '/': {
return  getRidOfRightZeroes(divide(left, right, maxDecimals));
}
case  '+': {
return  getRidOfRightZeroes(add(left, right, maxDecimals));
}
case  '-': {
return  getRidOfRightZeroes(substract(left, right, maxDecimals));
}
default:
throw  new  Error(`${op} Not implemented`);
}
}
function  handlePressDigit(digit) {
const  newState = {};
const  isDot = digit === '.';
if (op) {
if (right === '0' && digit === '0') {
return;
}
if (isDot && right && right.indexOf('.') !== -1) {
return;
}
newState.right =
right === '0' || !right ? (isDot ? '0.' : digit) : `${right}${digit}`;
} else {
if (left === '0' && digit === '0') {
return;
}
if (digit === '.' && left.indexOf('.') !== -1) {
return;
}
newState.left =
left === '0' || !left ? (isDot ? '0.' : digit) : `${left}${digit}`;
}
setState((prevState) => ({
...prevState,
...newState,
}));
}
function  getRidOfRightZeroes(result) {
const [, decimals] = result.split('.');
if (!decimals) {
return  result;
}
let  charsToRemove = 0;
const  len = result.length - 1;
for (let  i = len; i > -1; i--) {
const  lastChar = result[i];
if (lastChar === '0') {
charsToRemove++;
} else  if (lastChar === '.') {
charsToRemove++;
break;
} else {
break;
}
}
if (!charsToRemove) {
return  result;
}
return  result.slice(0, -charsToRemove);
}
function  divide(left, right, decimals = 0) {
if (left === '0' || right === '0') {
return  '0';
}
return (+left / +right).toFixed(decimals);
}
function  multiply(left, right, decimals = 0) {
if (left === '0' || right === '0') {
return  '0';
}
return (+left * +right).toFixed(decimals);
}
function  add(left, right, decimals = 0) {
return (+left + +right).toFixed(decimals);
}
function  substract(left, right, decimals = 0) {
return (+left - +right).toFixed(decimals);
}
function  restart(initialValue = '0') {
setState({
...INITIAL_STATE,
left:  '' + initialValue,
});
}
function  handlePressOp(nextOp) {
let  nextState = {};
if (op && right) {
const  result = calc(left, op, right);
nextState = {
right:  null,
left:  result,
history: [1],
};
}
setState((prevState) => ({
...prevState,
...nextState,
op:  nextOp,
}));
}
function  handlePressSwitchSign() {
const  nextState = {};
if (op) {
if (right === '0') {
return;
}
nextState.right = right.startsWith('-') ? right.slice(1) : '-' + right;
} else {
if (left === '0') {
return;
}
nextState.left = left.startsWith('-') ? left.slice(1) : '-' + left;
}
setState((prevState) => ({
...prevState,
...nextState,
}));
}
function  handlePressPercentage() {
const  nextState = {};
if (op && right) {
nextState.right = `${+right / 100}`;
} else {
nextState.left = `${+left / 100}`;
}
setState((prevState) => ({
...prevState,
...nextState,
}));
}
function  equal() {
let  nextState = null;
if (op && right) {
nextState = {
left:  calc(left, op, right),
right:  null,
lastOp: {
op,
right,
},
};
} else  if (lastOp) {
const {op, right} = lastOp;
nextState = {
left:  calc(left, op, right),
right:  null,
};
} else  if (op) {
nextState = {
left:  calc(left, op, left),
right:  null,
lastOp: {
op,
right:  left,
},
};
}
if (!nextState) {
return;
}
setState((prevState) => ({
...prevState,
...nextState,
}));
}
function  debug() {
return  JSON.stringify({left, op, right}, null, '\t');
}
function  handlePressPinkishTheme() {
setTheme(THEME.PINKISH);
}
function  handlePressGreenishTheme() {
setTheme(THEME.GREENISH);
}
const {
mainScreen: mainScreenStyles,
lowerContainer: lowerContainerStyles,
adornment: adornmentStyles,
} = mapStateAndPropsToStyles({
isDarkish,
style,
lowerContainerStyle,
});
return (
<View  style={mainScreenStyles}>
<View  style={styles.themesContainer}>
<TouchableOpacity  onPress={handlePressPinkishTheme}>
<LinearGradient  style={styles.theme}  colors={COLOR.PINKISH}  />
</TouchableOpacity>
<TouchableOpacity  onPress={handlePressGreenishTheme}>
<LinearGradient  style={styles.theme}  colors={COLOR.GREENISH}  />
</TouchableOpacity>
</View>
<View  style={styles.upperContainer}>
{/* <View>
<Text>{debug()}</Text>
</View> */}
<View  style={styles.historyContainer}>
{history.map((result, i) => {
return (
<Text
size="sm"
key={`${i}-${result}`}
onPress={() =>  restart(result)}>
{result}
</Text>
);
})}
</View>
<Text  size="lg">{right || left}</Text>
<View  style={adornmentStyles}  />
</View>
<View  style={lowerContainerStyles}>
<View  style={styles.row}>
<Button  onPress={() =>  restart(0)}>C</Button>
<Button  onPress={() =>  handlePressSwitchSign()}>+/-</Button>
<Button  onPress={() =>  handlePressPercentage()}>%</Button>
<Button  onPress={() =>  handlePressOp('/')}>/</Button>
</View>
<View  style={styles.row}>
<Button  onPress={() =>  handlePressDigit('7')}>7</Button>
<Button  onPress={() =>  handlePressDigit('8')}>8</Button>
<Button  onPress={() =>  handlePressDigit('9')}>9</Button>
<Button  onPress={() =>  handlePressOp('*')}>x</Button>
</View>
<View  style={styles.row}>
<Button  onPress={() =>  handlePressDigit('4')}>4</Button>
<Button  onPress={() =>  handlePressDigit('5')}>5</Button>
<Button  onPress={() =>  handlePressDigit('6')}>6</Button>
<Button  onPress={() =>  handlePressOp('+')}>+</Button>
</View>
<View  style={styles.row}>
<Button  onPress={() =>  handlePressDigit('1')}>1</Button>
<Button  onPress={() =>  handlePressDigit('2')}>2</Button>
<Button  onPress={() =>  handlePressDigit('3')}>3</Button>
<Button  onPress={() =>  handlePressOp('-')}>-</Button>
</View>
<View  style={styles.row}>
<Button  onPress={() =>  handlePressDigit('0')}>0</Button>
<Button  onPress={() =>  handlePressDigit('.')}>.</Button>
<MainButton  style={styles.mainButton}  onPress={equal}>
=
</MainButton>
</View>
</View>
</View>
);
}
MainScreen.propTypes = {
	style:  PropTypes.oneOfType([
		PropTypes.number,
		PropTypes.object,
		PropTypes.arrayOf(PropTypes.number),
		PropTypes.arrayOf(PropTypes.object),
	]),
	lowerContainerStyle:  PropTypes.oneOfType([
	PropTypes.number,
	PropTypes.object,
	PropTypes.arrayOf(PropTypes.number),
	PropTypes.arrayOf(PropTypes.object),
	]),
};
export  default  MainScreen;

By – Cesar Rodriguez Chavez, Senior Software Engineer, DigitalOnUs

Related posts