diff options
author | A404M <ahmadmahmoudiprogrammer@gmail.com> | 2024-12-18 03:28:02 +0330 |
---|---|---|
committer | A404M <ahmadmahmoudiprogrammer@gmail.com> | 2024-12-18 03:28:02 +0330 |
commit | d4573cacc141748c44bb6bbea7528bdd46afc3d5 (patch) | |
tree | d5915bd32b2e58bae60ca906ec9475656c8a0e87 /app/src/main/java/com |
initial commit
Diffstat (limited to 'app/src/main/java/com')
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 |