summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/a404m/calculator/MainActivity.kt27
-rw-r--r--app/src/main/java/com/a404m/calculator/MathHelper.kt309
-rw-r--r--app/src/main/java/com/a404m/calculator/model/CalcButtonModel.kt139
-rw-r--r--app/src/main/java/com/a404m/calculator/ui/page/Home.kt120
-rw-r--r--app/src/main/java/com/a404m/calculator/ui/theme/Color.kt11
-rw-r--r--app/src/main/java/com/a404m/calculator/ui/theme/Theme.kt58
-rw-r--r--app/src/main/java/com/a404m/calculator/ui/theme/Type.kt34
-rw-r--r--app/src/main/java/com/a404m/calculator/ui/util/Container.kt31
8 files changed, 729 insertions, 0 deletions
diff --git a/app/src/main/java/com/a404m/calculator/MainActivity.kt b/app/src/main/java/com/a404m/calculator/MainActivity.kt
new file mode 100644
index 0000000..9a40e9b
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/MainActivity.kt
@@ -0,0 +1,27 @@
+package com.a404m.calculator
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.a404m.calculator.ui.page.HomePage
+import com.a404m.calculator.ui.theme.CalculatorTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ CalculatorTheme {
+ HomePage()
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/calculator/MathHelper.kt b/app/src/main/java/com/a404m/calculator/MathHelper.kt
new file mode 100644
index 0000000..fa6cfc7
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/MathHelper.kt
@@ -0,0 +1,309 @@
+package com.a404m.calculator
+
+import android.util.Log
+import com.a404m.calculator.MathHelper.Operator.Kind
+import java.nio.charset.UnsupportedCharsetException
+
+object MathHelper {
+ private val operatorStrings = arrayOf(
+ '+',
+ '-',
+ '×',
+ '÷',
+ '%',
+ )
+ private val numberStrings = arrayOf(
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '.',
+ )
+
+ private val plus = Operator(
+ '+',
+ Kind.PREFIX,
+ {
+ Operand(it.first().value)
+ }
+ )
+ private val minus = Operator(
+ '-',
+ Kind.PREFIX,
+ {
+ Operand(-it.first().value)
+ }
+ )
+ private val add = Operator(
+ '+',
+ Kind.INFIX,
+ {
+ Operand(it.first().value+it.last().value)
+ }
+ )
+ private val sub = Operator(
+ '-',
+ Kind.INFIX,
+ {
+ Operand(it.first().value-it.last().value)
+ }
+ )
+ private val mul = Operator(
+ '×',
+ Kind.INFIX,
+ {
+ Operand(it.first().value*it.last().value)
+ }
+ )
+ private val div = Operator(
+ '÷',
+ Kind.INFIX,
+ {
+ Operand(it.first().value/it.last().value)
+ }
+ )
+ private val rem = Operator(
+ '%',
+ Kind.INFIX,
+ {
+ Operand(it.first().value%it.last().value)
+ }
+ )
+ private val operatorOrders = arrayOf(
+ arrayOf(
+ plus,
+ minus,
+ ),
+ arrayOf(
+ mul,
+ div,
+ rem,
+ ),
+ arrayOf(
+ add,
+ sub,
+ )
+ )
+ private val operators = arrayOf(
+ plus,
+ minus,
+ add,
+ sub,
+ mul,
+ div,
+ rem,
+ )
+
+ fun eval(expression: String): String {
+ return try {
+ parse(lex(expression)).eval().toString()
+ } catch (e: Exception) {
+ e.message ?: "Exception"
+ }
+ }
+
+ private fun parse(lexed: ArrayList<Any>): Operator {
+ for (order in operatorOrders) {
+ if (lexed.size == 1) {
+ break
+ }
+ for (i in lexed.indices) {
+ val item = lexed[i]
+ if (item is BasicOperator) {
+ var op = item.toOperator(
+ lexed,
+ i
+ )
+ if (op in order) {
+ op = op.cloneWithoutOperands()
+ Log.d(
+ "A404M",
+ "parse: $op"
+ )
+ lexed[i] = op
+ when (op.kind) {
+ Kind.NONE -> {}
+ Kind.INFIX -> {
+ op.operands.add(lexed.removeAt(i - 1))
+ op.operands.add(lexed.removeAt(i))
+ }
+
+ Kind.PREFIX -> {
+ op.operands.add(lexed.removeAt(i + 1))
+ }
+
+ Kind.POSTFIX -> {
+ op.operands.add(lexed.removeAt(i - 1))
+ }
+ }
+ break
+ }
+ }
+ }
+ }
+ return if (lexed.size != 1) {
+ throw UnsupportedOperationException("lexed.size = ${lexed.size}: $lexed")
+ } else if (lexed.first() is Operator) {
+ lexed.first() as Operator
+ } else if (lexed.first() is Operand) {
+ plus.cloneWithoutOperands().cloneWithOperator(arrayListOf(lexed.first()))
+ } else {
+ throw UnsupportedOperationException("unsupported type ${lexed.first().javaClass}")
+ }
+ }
+
+ private fun lex(expression: String): ArrayList<Any> {
+ val items = arrayListOf<Any>()
+
+ var i = 0
+ while (i < expression.length) {
+ when (val c = expression[i]) {
+ in numberStrings -> {
+ val start = i
+ while (++i < expression.length && expression[i] in numberStrings);
+ items.add(
+ Operand(
+ value = expression.substring(
+ start,
+ i
+ ).toDouble()
+ )
+ )
+ --i
+ }
+
+ in operatorStrings -> {
+ items.add(
+ BasicOperator(c)
+ )
+ }
+
+ else -> throw UnsupportedCharsetException(c.toString())
+ }
+ ++i
+ }
+
+ return items
+ }
+
+ private class Operator(
+ val operator: Char,
+ val kind: Kind,
+ val operate: (operands:List<Operand>)->Operand,
+ val operands: ArrayList<Any> = arrayListOf(),
+ ) {
+ fun cloneWithoutOperands():Operator{
+ return Operator(
+ operator,
+ kind,
+ operate,
+ )
+ }
+
+ fun cloneWithOperator(operands: ArrayList<Any>): Operator {
+ return Operator(
+ operator,
+ kind,
+ operate,
+ operands,
+ )
+ }
+
+ fun eval(): Operand {
+ Log.d(
+ "A404M",
+ "eval: $operator $operands"
+ )
+ if(operandSize() != operands.size){
+ throw UnsupportedOperationException("Not enough operands")
+ }
+
+ val evaledOperands = operands.map {
+ if(it is Operator){
+ it.eval()
+ }else{
+ it as Operand
+ }
+ }
+
+ return operate(evaledOperands)
+ }
+
+ fun operandSize(): Int {
+ return when (kind) {
+ Kind.INFIX -> 2
+ Kind.PREFIX, Kind.POSTFIX -> 1
+ Kind.NONE -> 0
+ }
+ }
+
+ override fun toString(): String {
+ return "$operator $kind $operands"
+ }
+
+ enum class Kind {
+ NONE,
+ INFIX,
+ PREFIX,
+ POSTFIX,
+ }
+ }
+
+ private class BasicOperator(
+ val operator: Char,
+ ) {
+ fun toOperator(
+ items: List<Any>,
+ i: Int
+ ): Operator {
+ val left = items.getOrElse(
+ i - 1,
+ { Any() },
+ ) is Operand
+
+ val right = items.getOrElse(
+ i + 1,
+ { Any() },
+ ) is Operand
+
+ val k:Kind
+
+ if (left) {
+ if (right) {
+ k = Kind.INFIX
+ } else {
+ k = Kind.POSTFIX
+ }
+ } else if (right) {
+ k = Kind.PREFIX
+ } else {
+ k = Kind.NONE
+ }
+
+ return operators.first {
+ it.operator == operator && it.kind == k
+ }
+ }
+
+ override fun toString(): String {
+ return operator.toString();
+ }
+ }
+
+ private class Operand(
+ val value: Double,
+ ){
+ override fun toString(): String {
+ if(value % 1 == 0.0){
+ return "%.0f".format(value)
+ }
+ return value.toString()
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/calculator/model/CalcButtonModel.kt b/app/src/main/java/com/a404m/calculator/model/CalcButtonModel.kt
new file mode 100644
index 0000000..435acef
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/model/CalcButtonModel.kt
@@ -0,0 +1,139 @@
+package com.a404m.calculator.model
+
+import com.a404m.calculator.MathHelper
+
+data class CalcButtonModel(
+ val text: String,
+ val weight:Float = 1F,
+ val op: (expression: String) -> String,
+) {
+ companion object {
+ val K_0 = CalcButtonModel(
+ text = "0",
+ op = {
+ it + '0'
+ }
+ )
+ val K_1 = CalcButtonModel(
+ text = "1",
+ op = {
+ it + '1'
+ }
+ )
+ val K_2 = CalcButtonModel(
+ text = "2",
+ op = {
+ it + '2'
+ }
+ )
+ val K_3 = CalcButtonModel(
+ text = "3",
+ op = {
+ it + '3'
+ }
+ )
+ val K_4 = CalcButtonModel(
+ text = "4",
+ op = {
+ it + '4'
+ }
+ )
+ val K_5 = CalcButtonModel(
+ text = "5",
+ op = {
+ it + '5'
+ }
+ )
+ val K_6 = CalcButtonModel(
+ text = "6",
+ op = {
+ it + '6'
+ }
+ )
+ val K_7 = CalcButtonModel(
+ text = "7",
+ op = {
+ it + '7'
+ }
+ )
+ val K_8 = CalcButtonModel(
+ text = "8",
+ op = {
+ it + '8'
+ }
+ )
+ val K_9 = CalcButtonModel(
+ text = "9",
+ op = {
+ it + '9'
+ }
+ )
+ val K_PLUS = CalcButtonModel(
+ text = "+",
+ op = {
+ it + '+'
+ }
+ )
+ val K_MINUS = CalcButtonModel(
+ text = "-",
+ op = {
+ it + '-'
+ }
+ )
+ val K_MULTIPLY = CalcButtonModel(
+ text = "×",
+ op = {
+ it + '×'
+ }
+ )
+ val K_DIVISION = CalcButtonModel(
+ text = "÷",
+ op = {
+ it + '÷'
+ }
+ )
+ val K_REMINDER = CalcButtonModel(
+ text = "%",
+ op = {
+ it + '%'
+ }
+ )
+ val K_EQUAL = CalcButtonModel(
+ text = "=",
+ op = {
+ MathHelper.eval(it)
+ }
+ )
+ val K_DOT = CalcButtonModel(
+ text = ".",
+ op = {
+ it + '.'
+ }
+ )
+ val K_C = CalcButtonModel(
+ text = "C",
+ op = {
+ ""
+ }
+ )
+ val K_BACKSPACE = CalcButtonModel(
+ text = "⌫",
+ op = {
+ if (it.length <= 1) {
+ ""
+ } else {
+ it.substring(
+ 0,
+ it.length - 1,
+ )
+ }
+ }
+ )
+ val K_SWITCH_MODE = CalcButtonModel(
+ text = "\u2026",
+ op = {
+ it
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/a404m/calculator/ui/page/Home.kt b/app/src/main/java/com/a404m/calculator/ui/page/Home.kt
new file mode 100644
index 0000000..ba9a21e
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/ui/page/Home.kt
@@ -0,0 +1,120 @@
+package com.a404m.calculator.ui.page
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.a404m.calculator.model.CalcButtonModel
+import com.a404m.calculator.ui.theme.CalculatorTheme
+import com.a404m.calculator.ui.util.CalcGrid
+
+@Composable
+fun HomePage(modifier: Modifier = Modifier) {
+ var expression by rememberSaveable {
+ mutableStateOf("")
+ }
+ Scaffold(
+ modifier = modifier,
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.Bottom,
+ ) {
+ Text(
+ modifier = Modifier.padding(8.dp).fillMaxWidth(),
+ text = expression,
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Right,
+ fontSize = 35.sp,
+ )
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp))
+ CalcGrid(
+ modifier = Modifier
+ .padding(4.dp),
+ columns = 4,
+ items = arrayOf(
+ CalcButtonModel.K_C,
+ CalcButtonModel.K_BACKSPACE,
+ CalcButtonModel.K_REMINDER,
+ CalcButtonModel.K_DIVISION,
+ CalcButtonModel.K_7,
+ CalcButtonModel.K_8,
+ CalcButtonModel.K_9,
+ CalcButtonModel.K_MULTIPLY,
+ CalcButtonModel.K_4,
+ CalcButtonModel.K_5,
+ CalcButtonModel.K_6,
+ CalcButtonModel.K_MINUS,
+ CalcButtonModel.K_1,
+ CalcButtonModel.K_2,
+ CalcButtonModel.K_3,
+ CalcButtonModel.K_PLUS,
+ CalcButtonModel.K_0,
+ CalcButtonModel.K_DOT,
+ CalcButtonModel.K_SWITCH_MODE,
+ CalcButtonModel.K_EQUAL,
+ ),
+ ) {
+ val backColor = MaterialTheme.colorScheme.primary
+ Box(
+ modifier = Modifier
+ .padding(4.dp)
+ .height(70.dp)
+ .weight(it.weight)
+ .clip(
+ shape = RoundedCornerShape(10.dp),
+ )
+ .background(
+ color = backColor,
+ shape = RoundedCornerShape(10.dp),
+ )
+ .clickable {
+ expression = it.op(expression)
+ },
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = it.text,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.contentColorFor(backColor)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun HomePagePreview() {
+ CalculatorTheme {
+ HomePage(
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/calculator/ui/theme/Color.kt b/app/src/main/java/com/a404m/calculator/ui/theme/Color.kt
new file mode 100644
index 0000000..6a4fd22
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.a404m.calculator.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260) \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/calculator/ui/theme/Theme.kt b/app/src/main/java/com/a404m/calculator/ui/theme/Theme.kt
new file mode 100644
index 0000000..20d6120
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.a404m.calculator.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun CalculatorTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/calculator/ui/theme/Type.kt b/app/src/main/java/com/a404m/calculator/ui/theme/Type.kt
new file mode 100644
index 0000000..42ec494
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.a404m.calculator.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+) \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/calculator/ui/util/Container.kt b/app/src/main/java/com/a404m/calculator/ui/util/Container.kt
new file mode 100644
index 0000000..ba27afe
--- /dev/null
+++ b/app/src/main/java/com/a404m/calculator/ui/util/Container.kt
@@ -0,0 +1,31 @@
+package com.a404m.calculator.ui.util
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun <T> CalcGrid(
+ modifier: Modifier = Modifier,
+ columns: Int,
+ items: Array<T>,
+ builder: @Composable (T) -> Unit,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ var index = 0
+ while (index != items.size) {
+ Row {
+ for (j in 0 until columns) {
+ if(index == items.size){
+ break
+ }
+ builder(items[index])
+ ++index
+ }
+ }
+ }
+ }
+} \ No newline at end of file