From 1b7795d4febc63543fa99cfcd99494f4197c7f42 Mon Sep 17 00:00:00 2001 From: Alexey Utovka Date: Wed, 10 Apr 2019 10:00:19 +0300 Subject: [PATCH] Initial commit. Simple string calculator based on Clean Architecture. Layers: *CORE >>> calculating string expression *UI >>> user input handling, calculation result handling Features: *simple arithmetic operations (addition, subtraction, multiplication, division) *unary subtraction *operation's priority assignment --- .gitignore | 13 + .idea/codeStyles/Project.xml | 29 ++ .idea/encodings.xml | 6 + .idea/gradle.xml | 15 + .idea/misc.xml | 49 +++ .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 33 ++ app/proguard-rules.pro | Bin 0 -> 1043 bytes app/src/main/AndroidManifest.xml | 24 ++ app/src/main/app_icon-web.png | Bin 0 -> 15330 bytes app/src/main/application_icon-web.png | Bin 0 -> 16927 bytes .../core/gateways/IUIResultHandler.java | 14 + .../calculator/core/models/IMathUnit.java | 13 + .../calculator/core/models/Number.java | 62 +++ .../calculator/core/models/Operator.java | 130 +++++++ .../calculator/core/threading/Executor.java | 57 +++ .../calculator/core/threading/IExecutor.java | 20 + .../core/usecases/CalculateExpression.java | 166 ++++++++ .../core/usecases/MathUnitsStack.java | 38 ++ .../core/usecases/SendResultRunnable.java | 27 ++ .../core/usecases/base/AbstractUseCase.java | 46 +++ .../core/usecases/base/IUseCase.java | 17 + .../calculator/ui/models/ExpressionModel.java | 68 ++++ .../ui/models/KeyboardInputModel.java | 118 ++++++ .../base/IActivityStateSupport.java | 17 + .../presenters/calculator/CommandHandler.java | 32 ++ .../calculator/ExpressionPresenter.java | 137 +++++++ .../ui/presenters/calculator/ICalculator.java | 46 +++ .../presenters/calculator/InputValidator.java | 89 +++++ .../calculator/ui/threading/Thread.java | 41 ++ .../ui/views/base/AbstractView.java | 23 ++ .../ui/views/base/StartPointActivity.java | 67 ++++ .../ui/views/calculator/CalculatorView.java | 71 ++++ .../calculator/SoftKeyboardListener.java | 43 +++ app/src/main/res/drawable/app_icon.png | Bin 0 -> 14191 bytes app/src/main/res/layout/calculator.xml | 358 ++++++++++++++++++ app/src/main/res/layout/wrapper.xml | 11 + app/src/main/res/mipmap-hdpi/app_icon.png | Bin 0 -> 1875 bytes app/src/main/res/mipmap-mdpi/app_icon.png | Bin 0 -> 1086 bytes app/src/main/res/mipmap-xhdpi/app_icon.png | Bin 0 -> 1976 bytes app/src/main/res/mipmap-xxhdpi/app_icon.png | Bin 0 -> 3140 bytes app/src/main/res/mipmap-xxxhdpi/app_icon.png | Bin 0 -> 4528 bytes .../values/application_icon_background.xml | 4 + app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/strings.xml | 23 ++ app/src/main/res/values/styles.xml | 10 + build.gradle | 29 ++ gradle.properties | Bin 0 -> 975 bytes gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++++ gradlew.bat | 84 ++++ settings.gradle | 1 + 55 files changed, 2236 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/app_icon-web.png create mode 100644 app/src/main/application_icon-web.png create mode 100644 app/src/main/java/ru/neurospb/calculator/core/gateways/IUIResultHandler.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/models/IMathUnit.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/models/Number.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/models/Operator.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/threading/Executor.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/threading/IExecutor.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/usecases/CalculateExpression.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/usecases/MathUnitsStack.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/usecases/SendResultRunnable.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/usecases/base/AbstractUseCase.java create mode 100644 app/src/main/java/ru/neurospb/calculator/core/usecases/base/IUseCase.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/models/ExpressionModel.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/models/KeyboardInputModel.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/presenters/base/IActivityStateSupport.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/CommandHandler.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ExpressionPresenter.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ICalculator.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/InputValidator.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/threading/Thread.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/views/base/AbstractView.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/views/base/StartPointActivity.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/views/calculator/CalculatorView.java create mode 100644 app/src/main/java/ru/neurospb/calculator/ui/views/calculator/SoftKeyboardListener.java create mode 100644 app/src/main/res/drawable/app_icon.png create mode 100644 app/src/main/res/layout/calculator.xml create mode 100644 app/src/main/res/layout/wrapper.xml create mode 100644 app/src/main/res/mipmap-hdpi/app_icon.png create mode 100644 app/src/main/res/mipmap-mdpi/app_icon.png create mode 100644 app/src/main/res/mipmap-xhdpi/app_icon.png create mode 100644 app/src/main/res/mipmap-xxhdpi/app_icon.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/app_icon.png create mode 100644 app/src/main/res/values/application_icon_background.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b75303 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..2996d53 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..aecfa13 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a3591ca --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + defaultConfig { + applicationId "ru.neurospb.app_icon" + minSdkVersion 15 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:28.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + //V7 support libs + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + //material design support lib + implementation 'com.android.support:design:28.0.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..9d434df2b3bb57fbc8a3a8ba2a42e65f96fc1e37 GIT binary patch literal 1043 zcmeH_O>P@8428Q6kUJ2_u5icubk$9Y21QZ8{Xv%+i8G@_BPtYS!zg<9lConYo1UPv z8<70oP*H#; zaHXV(!Aet{8#{@VVTv~2YRt1W8$-orW8d|XPKy;|hOxyIhM8SRUCo;Bh+!p02q-S5 zx>31K0a!%J?RFb*kBJKM;BsJ!VeqtW=N1D#*Ob2%qExrf+QTAiy1p`nPZJ!e@g;*+ z)$mUoe?&aMA<6>xyA8Cg)D^Szl_yGx4vIB^%ASKeo{BA{BRn;iG)fC3xmqE+6+x(X z+`GNod5%SNZ9$%#?>qI*TV=j`L@fPp#dSokXUO#A^FY(^pn%!|!v!!*#KQr{z4`ua z#sEqilV0=fd;R@$TKD^lMPfBag|Q;<)tw$Q#f*@fiK;h6uID=v8#3V7=GpF5#ra1N zR_oQCMz`!lIF3y;!JTI6(Qdr?tjsSR`OS?1^Pj%vpx ds9mqWx1M$P#AvafO7;>j{$AL7^Z$FDz+Y+j3 + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/app_icon-web.png b/app/src/main/app_icon-web.png new file mode 100644 index 0000000000000000000000000000000000000000..3097c3e753e324fed248c5290e4b69ec40a4ddef GIT binary patch literal 15330 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�C6ODwYV@^g{PFnbCryxK#JcK zPvaUm+MjEQn?BNV)0o5&od#UP4W#d?boBL80vYY188%D&>j@gPhCng?Q{oveBG@w& zbkL2f(HK{_^eo3|TrQ`QK;sh)aC$>s&8oi^NS-MiB-VR<%e9&I+foH@pXUXEOb=>0= zI&u3)U1BgfWQkqz+J2&uyaYC|$IA&G8Am?jFkJ^Heb{i*h!49al`Er4$Apw*|B|~% zpI4ciCl6N8!}h|X=4Ifd=vi{u6ibW>Bk&|y0F)BgE~^+_$PVbT#mEk9Dz7k$$`Dy3 z!IZCc2=V~j|0QS^Rg^z_wO!jvy6uvTge4iObbr9lIzlG|Y9+ySPcjoNiCtdp*}R;| zz`fLh3ho0cNmJ@K#mUE`7vkao;#ho4G#d(4d0xz0kYTk!og;|s* zH8jPxtN4w=SWKxY#!fsX458qtj?OD)qTe+UR)|OdFK$2$i{2(K%=6%=;X0~aU#FZp z!(hFxyIrb_Vh@j|4LOL=_`d!B|Og8OdM8X82BocMq8cd56m aIJhL#(Pa-OC&h-cfOHbX-atW$D8WYosX+um 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/ru/neurospb/calculator/core/gateways/IUIResultHandler.java b/app/src/main/java/ru/neurospb/calculator/core/gateways/IUIResultHandler.java new file mode 100644 index 0000000..b36707d --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/gateways/IUIResultHandler.java @@ -0,0 +1,14 @@ +package ru.neurospb.calculator.core.gateways; + +/** + * GATEWAY for UI layer + * Функционал для получателя(обработчика) результата выполнения действий системы (use case) + * + * @author Алексей Утовка + * @version а1 + */ + +public interface IUIResultHandler { + //обратный вызов для передачи результата Действия в слой UI + void onResult(String result); +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/models/IMathUnit.java b/app/src/main/java/ru/neurospb/calculator/core/models/IMathUnit.java new file mode 100644 index 0000000..dc4cb4a --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/models/IMathUnit.java @@ -0,0 +1,13 @@ +package ru.neurospb.calculator.core.models; + +/** + * Базовый функционал для математических единиц. + * + * @author Алексей Утовка + * @version а1 + */ + +public interface IMathUnit { + //возвращает тип математической единицы + int getType(); +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/models/Number.java b/app/src/main/java/ru/neurospb/calculator/core/models/Number.java new file mode 100644 index 0000000..75fec7a --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/models/Number.java @@ -0,0 +1,62 @@ +package ru.neurospb.calculator.core.models; + +/** + * Model. + * Модель числа + * + * @author Алексей Утовка + * @version а1 + */ + +public class Number implements IMathUnit{ + private double value; + + public static final int TYPE_NUMBER = 20; + private static final char CHAR_0 = '0'; + private static final char CHAR_1 = '1'; + private static final char CHAR_2 = '2'; + private static final char CHAR_3 = '3'; + private static final char CHAR_4 = '4'; + private static final char CHAR_5 = '5'; + private static final char CHAR_6 = '6'; + private static final char CHAR_7 = '7'; + private static final char CHAR_8 = '8'; + private static final char CHAR_9 = '9'; + private static final char CHAR_POINT = '.'; + private static final char CHAR_EXPONENT = 'E'; + + //КОНСТРУКТОР + public Number(String stringValue) { + value = Double.parseDouble(stringValue); + } + Number(double value) { + this.value = value; + } + + //ИМПЛЕМЕНТАЦИЯ: IMathUnit + @Override + public int getType() { + return TYPE_NUMBER; + } + + //МЕТОДЫ КЛАССА + //GETTER: value + public double getValue() { + return value; + } + //Проверяет является ли символ частью числа + public static boolean isNumberPart(char c) { + return c == CHAR_0 || + c == CHAR_1 || + c == CHAR_2 || + c == CHAR_3 || + c == CHAR_4 || + c == CHAR_5 || + c == CHAR_6 || + c == CHAR_7 || + c == CHAR_8 || + c == CHAR_9 || + c == CHAR_POINT || + c == CHAR_EXPONENT; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/models/Operator.java b/app/src/main/java/ru/neurospb/calculator/core/models/Operator.java new file mode 100644 index 0000000..53900b1 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/models/Operator.java @@ -0,0 +1,130 @@ +package ru.neurospb.calculator.core.models; + +import ru.neurospb.calculator.core.usecases.MathUnitsStack; + +/** + * Model. + * Модель оператора. + * + * @author Алексей Утовка + * @version а1 + */ + +public class Operator implements IMathUnit{ + private int type; + private int weight; + + public static final int TYPE_OPERATOR = 10; + private static final int OPERATOR_ADD = 100; + private static final int OPERATOR_SUB = 101; + private static final int OPERATOR_MUL = 102; + private static final int OPERATOR_DIV = 103; + private static final int OPERATOR_UNARY_SUB = 104; + public static final int OPERATOR_OPEN_BRACKET = 105; + public static final int OPERATOR_CLOSE_BRACKET = 106; + private static final int WEIGHT_BRACKET = 1; + private static final int WEIGHT_ADDSUB = 2; + private static final int WEIGHT_MULDIV = 3; + private static final int WEIGHT_UNARY = 4; + private static final char CHAR_ADD = '+'; + public static final char CHAR_SUB = '-'; + private static final char CHAR_MUL = '*'; + private static final char CHAR_DIV = '/'; + public static final char CHAR_UNARY_SUB = 'u'; + public static final char CHAR_OPEN_BRACKET = '('; + public static final char CHAR_CLOSE_BRACKET = ')'; + + + //КОНСТРУКТОР + public Operator(char operatorChar) { + switch (operatorChar) { + case CHAR_ADD: + type = OPERATOR_ADD; + weight = WEIGHT_ADDSUB; + break; + case CHAR_SUB: + type = OPERATOR_SUB; + weight = WEIGHT_ADDSUB; + break; + case CHAR_MUL: + type = OPERATOR_MUL; + weight = WEIGHT_MULDIV; + break; + case CHAR_DIV: + type = OPERATOR_DIV; + weight = WEIGHT_MULDIV; + break; + case CHAR_UNARY_SUB: + type = OPERATOR_UNARY_SUB; + weight = WEIGHT_UNARY; + break; + case CHAR_OPEN_BRACKET: + type = OPERATOR_OPEN_BRACKET; + weight = WEIGHT_BRACKET; + break; + case CHAR_CLOSE_BRACKET: + type = OPERATOR_CLOSE_BRACKET; + weight = WEIGHT_BRACKET; + break; + } + } + + //ИМПЛЕМЕНТАЦИЯ: IMathUnit + @Override + public int getType() { + return TYPE_OPERATOR; + } + + //МЕТОДЫ КЛАССА + //GETTER: type + public int type() { + return type; + } + //GETTER: weight + public int weight() { + return weight; + } + /** + * Проверяет, является ли переданный символ представлением оператора + * + * @param c символ для проверки + * @return boolean значение результата проверки + */ + public static boolean isOperator (char c) { + return c == CHAR_ADD || + c == CHAR_SUB || + c == CHAR_MUL || + c == CHAR_DIV || + c == CHAR_OPEN_BRACKET || + c == CHAR_CLOSE_BRACKET; + } + /** + * Выполняет операцию согласно своему типу для чисел из стека. + * Результат операции кладёт в стек. + * + * @param stack стек с числами + */ + public void execute(MathUnitsStack stack) { + double operandR = stack.pop().getValue();//правый операнд + if (type == OPERATOR_UNARY_SUB)//унарная операция + operandR = -operandR; + else {//бинарная операция + double operandL = stack.pop().getValue();//левый операнд + switch (type) { + case OPERATOR_ADD: + operandR = operandL + operandR; + break; + case OPERATOR_SUB: + operandR = operandL - operandR; + break; + case OPERATOR_MUL: + operandR = operandL * operandR; + break; + case OPERATOR_DIV: + operandR = operandL / operandR; + break; + } + } + stack.push(new Number(operandR)); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/threading/Executor.java b/app/src/main/java/ru/neurospb/calculator/core/threading/Executor.java new file mode 100644 index 0000000..8517733 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/threading/Executor.java @@ -0,0 +1,57 @@ +package ru.neurospb.calculator.core.threading; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import ru.neurospb.calculator.core.usecases.base.AbstractUseCase; + +/** + * Контроллер потоков. + * + * @author Алексей Утовка + * @version а1 + */ + +public class Executor implements IExecutor { + private static volatile Executor singletonExecutor; + private ThreadPoolExecutor threadPoolExecutor; + + private static final int CORE_POOL_SIZE = 3; + private static final int MAX_POOL_SIZE = 5; + private static final long KEEP_ALIVE_TIME = 120; + private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + private static final LinkedBlockingQueue WORK_QUEUE = new LinkedBlockingQueue<>(); + + //КОНСТРУКТОР + private Executor() { + threadPoolExecutor = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TIME_UNIT, + WORK_QUEUE); + } + + //ИМПЛЕМЕНТАЦИЯ: IExecutor + @Override + public void execute(final AbstractUseCase abstractUseCase) { + threadPoolExecutor.submit(new Runnable() { + @Override + public void run() { + abstractUseCase.run(); + abstractUseCase.onFinished(); + } + }); + } + + //МЕТОДЫ КЛАССА + /** + * Возвращает синглтон себя. Если не был инициализирован, + * тогда инициализирует и возвращает себя. + */ + public static IExecutor getInstance() { + if (singletonExecutor == null) singletonExecutor = new Executor(); + return singletonExecutor; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/threading/IExecutor.java b/app/src/main/java/ru/neurospb/calculator/core/threading/IExecutor.java new file mode 100644 index 0000000..26134d1 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/threading/IExecutor.java @@ -0,0 +1,20 @@ +package ru.neurospb.calculator.core.threading; + +import ru.neurospb.calculator.core.usecases.base.AbstractUseCase; + +/** + * Функционал контроллера потоков. + * + * @author Алексей Утовка + * @version а1 + */ + +public interface IExecutor { + //обеспечивает исполнение use case в фоновом потоке приложения + void execute(AbstractUseCase abstractUseCase); + + interface UIThread { + //обеспечивает исполнение кода в UI (основном) потоке приложения + void post(final Runnable runnable); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/usecases/CalculateExpression.java b/app/src/main/java/ru/neurospb/calculator/core/usecases/CalculateExpression.java new file mode 100644 index 0000000..eeb0e60 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/usecases/CalculateExpression.java @@ -0,0 +1,166 @@ +package ru.neurospb.calculator.core.usecases; + +import java.util.ArrayList; +import java.util.List; + +import ru.neurospb.calculator.core.gateways.IUIResultHandler; +import ru.neurospb.calculator.core.models.IMathUnit; +import ru.neurospb.calculator.core.models.Number; +import ru.neurospb.calculator.core.models.Operator; +import ru.neurospb.calculator.core.usecases.base.AbstractUseCase; + +import static ru.neurospb.calculator.core.models.Number.TYPE_NUMBER; +import static ru.neurospb.calculator.core.models.Operator.CHAR_CLOSE_BRACKET; +import static ru.neurospb.calculator.core.models.Operator.CHAR_OPEN_BRACKET; +import static ru.neurospb.calculator.core.models.Operator.CHAR_SUB; +import static ru.neurospb.calculator.core.models.Operator.CHAR_UNARY_SUB; +import static ru.neurospb.calculator.core.models.Operator.OPERATOR_CLOSE_BRACKET; +import static ru.neurospb.calculator.core.models.Operator.OPERATOR_OPEN_BRACKET; +import static ru.neurospb.calculator.core.models.Operator.TYPE_OPERATOR; + +/** + * USE CASE + * Вычисление результата строкового выражения + * + * @author Алексей Утовка + * @version а1 + */ + +public class CalculateExpression extends AbstractUseCase { + private IUIResultHandler handlerUI; + private String expression; + + //КОНСТРУКТОР + public CalculateExpression(String expression, + IUIResultHandler handlerUI) { + super(); + this.expression = expression; + this.handlerUI = handlerUI; + } + + //ИМПЛЕМЕНТАЦИЯ: IUseCase + @Override + public void run() { + List expressionPrepared = prepare(expression); + if (expressionPrepared == null) {//если в выражении ошибка (не правильно расставлены скобки) + getThreadUI().post(new SendResultRunnable(handlerUI, null)); + return; + } + List expressionRPN = convert2RPN(expressionPrepared); + getThreadUI().post(new SendResultRunnable(handlerUI, calculateRPN(expressionRPN))); + } + + //МЕТОДЫ КЛАССА + //парсит строку в списочный массив математических единиц + private List prepare(String expression) { + List preparedExpression = new ArrayList<>(); + StringBuilder numberBuffer = new StringBuilder();//буфер куда собираются все символы числа + char currentChar; + int counterBracketO = 0, counterBracketC = 0;//счётчики скобок + + for (int i = 0; i < expression.length(); i++) { + currentChar = expression.charAt(i); + if (Number.isNumberPart(currentChar))//если символ - часть числа >>> в буфер + numberBuffer.append(currentChar); + if (Operator.isOperator(currentChar)) {//если символ - оператор + //если собирали символы числа >>> собираем из них число и добавляем в массив + if (numberBuffer.length() > 0) { + preparedExpression.add(new Number(numberBuffer.toString())); + numberBuffer.delete(0, numberBuffer.length()); + } + //проверяем что символ(-ы) оператора[+,-,*,/] не последний(-ие) в строке + if (i == expression.length()-1 && + currentChar != CHAR_OPEN_BRACKET && + currentChar != CHAR_CLOSE_BRACKET) { + if (currentChar == CHAR_SUB && + preparedExpression.get(preparedExpression.size()-1).getType() == TYPE_OPERATOR) + preparedExpression.remove(preparedExpression.size()-1); + break; + } + //обновляем счётчики скобок, если соответствующие символы + if (currentChar == CHAR_OPEN_BRACKET) + counterBracketO++; + if (currentChar == CHAR_CLOSE_BRACKET) + counterBracketC++; + //определяем унарный ли минус + if (currentChar == CHAR_SUB) { + IMathUnit previousItem = (preparedExpression.size() > 0) ? + preparedExpression.get(preparedExpression.size()-1) : + null; + if (previousItem == null || + (previousItem.getType() == TYPE_OPERATOR && + ((Operator) previousItem).type() != OPERATOR_CLOSE_BRACKET) + ) + currentChar = CHAR_UNARY_SUB; + } + //собираем оператор и добавляем в массив + preparedExpression.add(new Operator(currentChar)); + } + } + //проверка на корректность скобок + if (counterBracketO != counterBracketC) + return null; + //если остались не собиранные символы числа >>> собираем из них число и добавляем в массив + if (numberBuffer.length() > 0) + preparedExpression.add(new Number(numberBuffer.toString())); + + return preparedExpression; + } + //преобразовывает выражение в обратную польскую запись (ОПЗ или RPN) + private List convert2RPN(List preparedExpression) { + List expressionRPN = new ArrayList<>(); + MathUnitsStack stack = new MathUnitsStack<>();//стек для операторов + IMathUnit item; + Operator operator; + + for (int i = 0; i < preparedExpression.size(); i++) { + item = preparedExpression.get(i); + //если операнд(число) >>> поместить в ОПЗ + if (item.getType() == TYPE_NUMBER) + expressionRPN.add(item); + //если оператор >>> + if (item.getType() == TYPE_OPERATOR) { + operator = (Operator) item; + //если оператор '(' >>> в стек + if (operator.type() == OPERATOR_OPEN_BRACKET) + stack.push(operator); + //если оператор ')' >>> всё до '(' из стека в ОПЗ; '(' удалить из стека + else if (operator.type() == OPERATOR_CLOSE_BRACKET) { + while (stack.last().type() != OPERATOR_OPEN_BRACKET) + expressionRPN.add(stack.pop()); + if (stack.last().type() == OPERATOR_OPEN_BRACKET) + stack.pop(); + } + //если оператор[+,-,*,/,u] >>> + else { + //если приоритет оператора меньше приоритета оператора из стека >>> из стека в ОПЗ + while (stack.notEmpty() && stack.last().weight() >= operator.weight()) + expressionRPN.add(stack.pop()); + //иначе добавить в стек + stack.push(operator); + } + } + } + //если в стеке есть операторы >>> в ОПЗ + while (stack.notEmpty()) + expressionRPN.add(stack.pop()); + + return expressionRPN; + } + //вычисление ОПЗ + private String calculateRPN(List expressionRPN) { + MathUnitsStack stack = new MathUnitsStack<>();//стек для операндов + IMathUnit item; + + for (int i = 0; i < expressionRPN.size(); i++) { + item = expressionRPN.get(i); + if (item.getType() == TYPE_NUMBER) + stack.push((Number) item); + else { + ((Operator) item).execute(stack); + } + } + + return Double.toString(stack.last().getValue()); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/usecases/MathUnitsStack.java b/app/src/main/java/ru/neurospb/calculator/core/usecases/MathUnitsStack.java new file mode 100644 index 0000000..99d6932 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/usecases/MathUnitsStack.java @@ -0,0 +1,38 @@ +package ru.neurospb.calculator.core.usecases; + +import java.util.ArrayList; +import java.util.List; + +import ru.neurospb.calculator.core.models.IMathUnit; + +/** + * Стек для математических единиц + * + * @author Алексей Утовка + * @version а1 + */ + +public class MathUnitsStack { + private List storage = new ArrayList<>(); + + //МЕТОДЫ КЛАССА + //добавление в стек + public void push(T item) { + storage.add(item); + } + //возвращает последний (верхний) элемент стека + T last() { + return storage.get(storage.size()-1); + } + //достаёт елемент из стека + public T pop() { + int lastId = storage.size()-1; + T item = storage.get(lastId); + storage.remove(lastId); + return item; + } + //проверяет пуст ли стек + boolean notEmpty() { + return storage.size() > 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/usecases/SendResultRunnable.java b/app/src/main/java/ru/neurospb/calculator/core/usecases/SendResultRunnable.java new file mode 100644 index 0000000..6ff4ed1 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/usecases/SendResultRunnable.java @@ -0,0 +1,27 @@ +package ru.neurospb.calculator.core.usecases; + +import ru.neurospb.calculator.core.gateways.IUIResultHandler; + +/** + * Сценарий отправки результата вычислений UI слою. + * + * @author Алексей Утовка + * @version а1 + */ + +public class SendResultRunnable implements Runnable { + private IUIResultHandler handlerUI; + private String resultString; + + //КОНСТРУКТОР + SendResultRunnable(IUIResultHandler handlerUI, String resultString) { + this.handlerUI = handlerUI; + this.resultString = resultString; + } + + //ИМПЛЕМЕНТАЦИЯ: Runnable + @Override + public void run() { + handlerUI.onResult(resultString); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/usecases/base/AbstractUseCase.java b/app/src/main/java/ru/neurospb/calculator/core/usecases/base/AbstractUseCase.java new file mode 100644 index 0000000..eedf9ab --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/usecases/base/AbstractUseCase.java @@ -0,0 +1,46 @@ +package ru.neurospb.calculator.core.usecases.base; + +import ru.neurospb.calculator.core.threading.Executor; +import ru.neurospb.calculator.core.threading.IExecutor; +import ru.neurospb.calculator.ui.threading.Thread; + +/** + * Базовый класс для UseCase. + * + * @author Алексей Утовка + * @version а1 + */ + +public abstract class AbstractUseCase implements IUseCase { + private volatile boolean isRunning; + private IExecutor multithreadingExecutor; + private IExecutor.UIThread threadUI; + + //КОНСТРУКТОР + public AbstractUseCase() { + threadUI = Thread.getInstance(); + multithreadingExecutor = Executor.getInstance(); + } + + //ИМПЛЕМЕНТАЦИЯ: IUseCase + @Override + public void execute() { + this.isRunning = true; + multithreadingExecutor.execute(this); + } + @Override + public void onFinished() { + isRunning = false; + } + + //GETTERS / SETTERS + //threadUI + protected IExecutor.UIThread getThreadUI() { + return threadUI; + } + //isRunning + @SuppressWarnings("unused") + public boolean isRunning() { + return isRunning; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/core/usecases/base/IUseCase.java b/app/src/main/java/ru/neurospb/calculator/core/usecases/base/IUseCase.java new file mode 100644 index 0000000..553838d --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/core/usecases/base/IUseCase.java @@ -0,0 +1,17 @@ +package ru.neurospb.calculator.core.usecases.base; + +/** + * Базовый функционал UseCase. + * + * @author Алексей Утовка + * @version а1 + */ + +public interface IUseCase { + //обеспечивает выполнение use case в фоновом потоке. + void execute(); + //логика use case. + void run(); + //вызывается в фоновом потоке по завершению .run() + void onFinished(); +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/models/ExpressionModel.java b/app/src/main/java/ru/neurospb/calculator/ui/models/ExpressionModel.java new file mode 100644 index 0000000..841cd2f --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/models/ExpressionModel.java @@ -0,0 +1,68 @@ +package ru.neurospb.calculator.ui.models; + +/** + * Model. + * Модель строкового выражения. + * Не понимает взаимосвязь своих данных, за их (данных модели) корректность отвечает обработчик модели. + * + * @author Алексей Утовка + * @version а1 + */ + +public class ExpressionModel { + public static final String RAW_EXPRESSION_KEY = "raw_expression_key"; + private static final String RAW_EXPRESSION_DEFAULT = "0.0"; + private StringBuffer rawExpression; + + public static final String EXPRESSION_FLAGS_KEY = "expression_flags_key"; + private static final boolean[] EXPRESSION_FLAGS_DEFAULT = new boolean[] + {true, //NO_EXPRESSION_FLAG + false, //NUMBER_FLAG + false, //FRACTION_FLAG + false, //OPERATOR_FLAG + false, //OPENED_BRACKETS_FLAG + false, //CLOSED_BRACKETS_FLAG + false, //POINT_FLAG + false};//SIGN_OPERATOR + public static final int NO_EXPRESSION_FLAG = 0; + public static final int NUMBER_FLAG = 1; + public static final int FRACTION_FLAG = 2; + public static final int OPERATOR_FLAG = 3; + public static final int OPENED_BRACKETS_FLAG = 4; + public static final int CLOSED_BRACKETS_FLAG = 5; + public static final int POINT_FLAG = 6; + public static final int SIGN_OPERATOR = 7; + private boolean[] flags; + + //КОНСТРУКТОР + public ExpressionModel() { + rawExpression = new StringBuffer(RAW_EXPRESSION_DEFAULT); + flags = EXPRESSION_FLAGS_DEFAULT.clone(); + } + + //МЕТОДЫ КЛАССА + //GETTER: rawExpression + public StringBuffer getRaw() { + return rawExpression; + } + //GETTER: flags + public boolean[] getFlags() { + return flags; + } + /** + * Sets a new data to the object. + * + * @param rawString A new expression string OR null for default + * @param flags A new set of expression flags OR null for default + */ + public void update (String rawString, boolean[] flags) { + if (rawString == null) + rawString = RAW_EXPRESSION_DEFAULT; + rawExpression.delete(0, rawExpression.length()); + rawExpression.append(rawString); + + if (flags == null) + flags = EXPRESSION_FLAGS_DEFAULT.clone(); + this.flags = flags; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/models/KeyboardInputModel.java b/app/src/main/java/ru/neurospb/calculator/ui/models/KeyboardInputModel.java new file mode 100644 index 0000000..7d0c046 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/models/KeyboardInputModel.java @@ -0,0 +1,118 @@ +package ru.neurospb.calculator.ui.models; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.ui.views.base.StartPointActivity; + +/** + * Model. + * Модель пользовательского ввода клавиатуры. + * + * @author Алексей Утовка + * @version а1 + */ + +public class KeyboardInputModel { + public static final int TYPE_NUMBER = 100; + public static final int TYPE_OPERATOR = 200; + public static final int TYPE_MODIFIER = 300; + + private char value; + private int type; + private int code; + + //КОНСТРУКТОР + public KeyboardInputModel(int code) { + this.code = code; + setTypeByCode(code); + setValueByCode(code); + } + + //МЕТОДЫ КЛАССА + //GETTER: code + public int getCode() { + return code; + } + //GETTER: type + public int getType() { + return type; + } + //GETTER: value + public char getValue() { + return value; + } + //определяет тип пользовательского ввода по коду кнопки(id) + private void setTypeByCode(int code) { + switch (code) { + default: + type = TYPE_NUMBER; + break; + case R.id.buttonDivide: + case R.id.buttonMultiply: + case R.id.buttonSubtract: + case R.id.buttonAdd: + type = TYPE_OPERATOR; + break; + case R.id.buttonOpenBracket: + case R.id.buttonCloseBracket: + type = TYPE_MODIFIER; + break; + } + } + //определяет значение пользовательского ввода по коду кнопки(id) + private void setValueByCode(int code) { + StartPointActivity activity = StartPointActivity.getInstance(); + switch (code) { + case R.id.button0: + value = activity.getString(R.string.number_zero).charAt(0); + break; + case R.id.button1: + value = activity.getString(R.string.number_one).charAt(0); + break; + case R.id.button2: + value = activity.getString(R.string.number_two).charAt(0); + break; + case R.id.button3: + value = activity.getString(R.string.number_three).charAt(0); + break; + case R.id.button4: + value = activity.getString(R.string.number_four).charAt(0); + break; + case R.id.button5: + value = activity.getString(R.string.number_five).charAt(0); + break; + case R.id.button6: + value = activity.getString(R.string.number_six).charAt(0); + break; + case R.id.button7: + value = activity.getString(R.string.number_seven).charAt(0); + break; + case R.id.button8: + value = activity.getString(R.string.number_eight).charAt(0); + break; + case R.id.button9: + value = activity.getString(R.string.number_nine).charAt(0); + break; + case R.id.buttonPoint: + value = activity.getString(R.string.symbol_point).charAt(0); + break; + case R.id.buttonDivide: + value = activity.getString(R.string.symbol_divide).charAt(0); + break; + case R.id.buttonMultiply: + value = activity.getString(R.string.symbol_multiply).charAt(0); + break; + case R.id.buttonSubtract: + value = activity.getString(R.string.symbol_subtract).charAt(0); + break; + case R.id.buttonAdd: + value = activity.getString(R.string.symbol_add).charAt(0); + break; + case R.id.buttonOpenBracket: + value = activity.getString(R.string.symbol_openbracket).charAt(0); + break; + case R.id.buttonCloseBracket: + value = activity.getString(R.string.symbol_closebracket).charAt(0); + break; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/presenters/base/IActivityStateSupport.java b/app/src/main/java/ru/neurospb/calculator/ui/presenters/base/IActivityStateSupport.java new file mode 100644 index 0000000..9d8b0d7 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/presenters/base/IActivityStateSupport.java @@ -0,0 +1,17 @@ +package ru.neurospb.calculator.ui.presenters.base; + +import android.os.Bundle; + +/** + * Поддержа сохранения/восстановления состояния приложения (через обратные вызовы Activity). + * + * @author Алексей Утовка + * @version а1 + */ + +public interface IActivityStateSupport { + //вызывается в Activity.onSaveInstanceState() + void saveState(Bundle state); + //вызывается в Activity.onCreate() + void loadState(Bundle state); +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/CommandHandler.java b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/CommandHandler.java new file mode 100644 index 0000000..37b3109 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/CommandHandler.java @@ -0,0 +1,32 @@ +package ru.neurospb.calculator.ui.presenters.calculator; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.core.usecases.CalculateExpression; + +/** + * ICalculator.CommandHandler + * Исполняет команды пользователя + * + * @author Алексей Утовка + * @version а1 + */ + +public class CommandHandler implements ICalculator.CommandHandler { + + //ИМПЛЕМЕНТАЦИЯ: ICalculator.CommandHandler + @Override + public void onCommand(int type) { + switch (type) { + case R.id.buttonDelete: + ExpressionPresenter.getInstance().flushExpression(); + break; + case R.id.buttonEqual: + CalculateExpression calculateExpressionUseCase = new CalculateExpression( + ExpressionPresenter.getInstance().getExpression().getRaw().toString(), + ExpressionPresenter.getInstance() + ); + calculateExpressionUseCase.execute(); + break; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ExpressionPresenter.java b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ExpressionPresenter.java new file mode 100644 index 0000000..d1396bf --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ExpressionPresenter.java @@ -0,0 +1,137 @@ +package ru.neurospb.calculator.ui.presenters.calculator; + +import android.os.Bundle; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.core.gateways.IUIResultHandler; +import ru.neurospb.calculator.ui.models.ExpressionModel; +import ru.neurospb.calculator.ui.models.KeyboardInputModel; +import ru.neurospb.calculator.ui.views.base.StartPointActivity; +import ru.neurospb.calculator.ui.views.calculator.CalculatorView; + +import static ru.neurospb.calculator.ui.models.ExpressionModel.CLOSED_BRACKETS_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.EXPRESSION_FLAGS_KEY; +import static ru.neurospb.calculator.ui.models.ExpressionModel.FRACTION_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.NO_EXPRESSION_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.NUMBER_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.OPENED_BRACKETS_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.OPERATOR_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.POINT_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.RAW_EXPRESSION_KEY; +import static ru.neurospb.calculator.ui.models.ExpressionModel.SIGN_OPERATOR; +import static ru.neurospb.calculator.ui.models.KeyboardInputModel.TYPE_NUMBER; +import static ru.neurospb.calculator.ui.models.KeyboardInputModel.TYPE_OPERATOR; + +/** + * ICalculator.ExpressionPresenter + * Управляет моделью строкового выражения. + * Управляет ICalculator.View + * + * @author Алексей Утовка + * @version а1 + */ + +public class ExpressionPresenter implements ICalculator.ExpressionPresenter, + IUIResultHandler { + private static volatile ICalculator.ExpressionPresenter selfSingleton; + private ICalculator.View view; + private ExpressionModel expression; + + //КОНСТРУКТОР + public ExpressionPresenter() { + selfSingleton = this; + expression = new ExpressionModel(); + view = new CalculatorView(); + view.showExpression(expression.getRaw().toString()); + } + + //ИМПЛЕМЕНТАЦИЯ: IActivityStateSupport + @Override + public void saveState(Bundle state) { + state.putString(RAW_EXPRESSION_KEY, expression.getRaw().toString()); + state.putBooleanArray(EXPRESSION_FLAGS_KEY, expression.getFlags()); + } + @Override + public void loadState(Bundle state) { + if (state != null) + expression.update(state.getString(RAW_EXPRESSION_KEY), + state.getBooleanArray(EXPRESSION_FLAGS_KEY)); + } + + //ИМПЛЕМЕНТАЦИЯ: ICalculator.ExpressionPresenter + @Override + public ExpressionModel getExpression() { + return expression; + } + @Override + public void onSuccessfulInputValidation(KeyboardInputModel inputtedKey) { + boolean[] expressionFlags = expression.getFlags(); + + //update expression string by first checking cases for autocomplete + /* + * CASE: user did not input digit before point >>> insert '0' before point + * ex.: "2+.2" >>> "2+0.2" + */ + if (inputtedKey.getCode() == R.id.buttonPoint && !expressionFlags[NUMBER_FLAG]) + expression.getRaw().append('0'); + /* + * CASE: user did not input digit after point >>> delete point + * ex.: "2+2.+" >>> "2+2+" + */ + if (expressionFlags[POINT_FLAG] && !(inputtedKey.getType() == TYPE_NUMBER)) + expression.getRaw().deleteCharAt(expression.getRaw().length()-1); + /* + * CASE: remove default template when user starts typing + */ + if (expressionFlags[NO_EXPRESSION_FLAG]) + expression.getRaw().delete(0, expression.getRaw().length()); + expression.getRaw().append(inputtedKey.getValue()); + + //updating expression flags + expressionFlags[NO_EXPRESSION_FLAG] = false; + expressionFlags[NUMBER_FLAG] = inputtedKey.getType() == TYPE_NUMBER; + expressionFlags[POINT_FLAG] = inputtedKey.getCode() == R.id.buttonPoint; + expressionFlags[FRACTION_FLAG] = (expressionFlags[POINT_FLAG] || + (expressionFlags[FRACTION_FLAG] && expressionFlags[NUMBER_FLAG])); + expressionFlags[SIGN_OPERATOR] = expressionFlags[OPERATOR_FLAG] && + inputtedKey.getCode() == R.id.buttonSubtract; + expressionFlags[OPERATOR_FLAG] = inputtedKey.getType() == TYPE_OPERATOR; + expressionFlags[OPENED_BRACKETS_FLAG] = inputtedKey.getCode() == R.id.buttonOpenBracket; + expressionFlags[CLOSED_BRACKETS_FLAG] = inputtedKey.getCode() == R.id.buttonCloseBracket; + + view.showExpression(expression.getRaw().toString()); + } + @Override + public void flushExpression() { + expression.update(null, null); + view.showExpression(expression.getRaw().toString()); + } + + //ИМПЛЕМЕНТАЦИЯ: IUIResultHandler + @Override + public void onResult(String result) { + if (result == null) + expression.update(StartPointActivity.getInstance().getString(R.string.error), null); + else { + expression.update(result, null); + //setup expression's flags according to result string. + boolean[] flags = expression.getFlags(); + flags[NO_EXPRESSION_FLAG] = false; + flags[NUMBER_FLAG] = true; + flags[FRACTION_FLAG] = result.contains(StartPointActivity.getInstance().getString(R.string.symbol_point)); + } + + view.showExpression(expression.getRaw().toString()); + } + + //МЕТОДЫ КЛАССА + /** + * Возвращает синглтон себя. Если не был инициализирован, + * тогда инициализирует и возвращает себя. + */ + public static ICalculator.ExpressionPresenter getInstance() { + if (selfSingleton == null)//just for lint. Has to be instantiated in runtime already. + selfSingleton = new ExpressionPresenter(); + return selfSingleton; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ICalculator.java b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ICalculator.java new file mode 100644 index 0000000..2333d0e --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/ICalculator.java @@ -0,0 +1,46 @@ +package ru.neurospb.calculator.ui.presenters.calculator; + +import ru.neurospb.calculator.core.gateways.IUIResultHandler; +import ru.neurospb.calculator.ui.models.ExpressionModel; +import ru.neurospb.calculator.ui.models.KeyboardInputModel; +import ru.neurospb.calculator.ui.presenters.base.IActivityStateSupport; + +/** + * Функционал калькулятора. + * + * @author Алексей Утовка + * @version а1 + */ + +public interface ICalculator { + //PRESENTERS + /** + * Манипулирование ExpressionModel + * Управление ICalculator.View + * Обработка (формат) результата вычисления. + */ + interface ExpressionPresenter extends IActivityStateSupport, IUIResultHandler { + ExpressionModel getExpression(); + void onSuccessfulInputValidation(KeyboardInputModel inputtedKey); + void flushExpression(); + } + //Валидация пользовательского ввода + interface InputValidator { + void validateInput(KeyboardInputModel inputModel); + } + //Исполнение пользовательских комманд + interface CommandHandler { + void onCommand(int type); + } + + //VIEWS + /** + * Отображение элементов UI калькулятора. + * Отображение пользовательского ввода. + * Отображение результатов вычисления выражения. + */ + interface View { + //Отображение ExpressionModel + void showExpression(String expressionString); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/InputValidator.java b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/InputValidator.java new file mode 100644 index 0000000..b9e98c4 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/presenters/calculator/InputValidator.java @@ -0,0 +1,89 @@ +package ru.neurospb.calculator.ui.presenters.calculator; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.ui.models.KeyboardInputModel; + +import static ru.neurospb.calculator.ui.models.ExpressionModel.CLOSED_BRACKETS_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.FRACTION_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.NO_EXPRESSION_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.NUMBER_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.OPENED_BRACKETS_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.OPERATOR_FLAG; +import static ru.neurospb.calculator.ui.models.ExpressionModel.SIGN_OPERATOR; +import static ru.neurospb.calculator.ui.models.KeyboardInputModel.TYPE_MODIFIER; +import static ru.neurospb.calculator.ui.models.KeyboardInputModel.TYPE_NUMBER; +import static ru.neurospb.calculator.ui.models.KeyboardInputModel.TYPE_OPERATOR; + +/** + * ICalculator.InputValidator + * Проверяет пользовательский ввод + * ЕСЛИ пользовательский ввод корректен + * ТО передаёт его обработчику строкового выражения + * ИНАЧЕ игнорирует пользовательский ввод + * + * @author Алексей Утовка + * @version а1 + */ + +public class InputValidator implements ICalculator.InputValidator { + + //ИМПЛЕМЕНТАЦИЯ: ICalculator.InputValidator + @Override + public void validateInput(KeyboardInputModel inputModel) { + boolean flagIsValidationSuccessful = false; + boolean[] expressionFlags = ExpressionPresenter.getInstance().getExpression().getFlags(); + + switch (inputModel.getType()) { + /* + * WRONG INPUT CHECK FOR KEYS[0-9, .]: + * after ')' + * duplicate '.' + */ + case TYPE_NUMBER: + if (expressionFlags[CLOSED_BRACKETS_FLAG]) break; + if (expressionFlags[FRACTION_FLAG] && + inputModel.getCode() == R.id.buttonPoint) + break; + flagIsValidationSuccessful = true; + break; + /* + * WRONG INPUT CHECK FOR KEYS[/, *, -, +]: + * first input '+', '*', '/' + * '+', '*', '/' after OPERATOR [+, *, /, -] OR '(' + * '-' after unary minus + */ + case TYPE_OPERATOR: + if (expressionFlags[NO_EXPRESSION_FLAG] && + inputModel.getCode() != R.id.buttonSubtract) break; + if ((expressionFlags[OPENED_BRACKETS_FLAG] || expressionFlags[OPERATOR_FLAG]) && + inputModel.getCode() != R.id.buttonSubtract) + break; + if (inputModel.getCode() == R.id.buttonSubtract && + expressionFlags[SIGN_OPERATOR]) + break; + flagIsValidationSuccessful = true; + break; + /* + * RIGHT INPUT CHECK FOR KEYS['(', ')']: + * '(' after OPERATOR [+, *, /, -] OR '(' + * first input '(' + * ')' after NUMBER [0-9, .] OR ')' + */ + case TYPE_MODIFIER: + if (inputModel.getCode() == R.id.buttonOpenBracket) { + if (expressionFlags[OPERATOR_FLAG] || + expressionFlags[NO_EXPRESSION_FLAG] || + expressionFlags[OPENED_BRACKETS_FLAG]) + flagIsValidationSuccessful = true; + } + if (inputModel.getCode() == R.id.buttonCloseBracket) { + if (expressionFlags[NUMBER_FLAG] || + expressionFlags[CLOSED_BRACKETS_FLAG]) + flagIsValidationSuccessful =true; + } + } + + if (flagIsValidationSuccessful) + ExpressionPresenter.getInstance().onSuccessfulInputValidation(inputModel); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/threading/Thread.java b/app/src/main/java/ru/neurospb/calculator/ui/threading/Thread.java new file mode 100644 index 0000000..8f2761f --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/threading/Thread.java @@ -0,0 +1,41 @@ +package ru.neurospb.calculator.ui.threading; + +import android.os.Handler; +import android.os.Looper; + +import ru.neurospb.calculator.core.threading.IExecutor; + +/** + * Обеспечивает выполнение переданого ему кода в UI (основном) потоке приложения. + * + * @author Алексей Утовка + * @version а1 + */ + +public class Thread implements IExecutor.UIThread { + private Handler mHandler; + private static Thread thread; + + //КОНСТРУКТОР + private Thread() { + mHandler = new Handler(Looper.getMainLooper()); + } + + //ИМПЛЕМЕНТАЦИЯ: IExecutor.UIThread + @Override + public void post(Runnable runnable) { + mHandler.post(runnable); + } + + //МЕТОДЫ КЛАССА + /** + * Возвращает синглтон себя. Если не был инициализирован, + * тогда инициализирует и возвращает себя. + */ + public static Thread getInstance() { + if (thread == null) { + thread = new Thread(); + } + return thread; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/views/base/AbstractView.java b/app/src/main/java/ru/neurospb/calculator/ui/views/base/AbstractView.java new file mode 100644 index 0000000..f270c2d --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/views/base/AbstractView.java @@ -0,0 +1,23 @@ +package ru.neurospb.calculator.ui.views.base; + +import android.support.design.widget.CoordinatorLayout; + +import ru.neurospb.calculator.R; + +/** + * Базовый класс для View. + * Подготавливает конкретный layout для конкретного view. + * + * @author Алексей Утовка + * @version а1 + */ + +public class AbstractView { + + //КОНСТРУКТОР + public AbstractView(int layoutId) { + CoordinatorLayout layoutWrapper = StartPointActivity.getInstance().findViewById(R.id.wrapper); + layoutWrapper.removeAllViews(); + StartPointActivity.getInstance().getLayoutInflater().inflate(layoutId, layoutWrapper); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/views/base/StartPointActivity.java b/app/src/main/java/ru/neurospb/calculator/ui/views/base/StartPointActivity.java new file mode 100644 index 0000000..f9e197f --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/views/base/StartPointActivity.java @@ -0,0 +1,67 @@ +package ru.neurospb.calculator.ui.views.base; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.ui.presenters.calculator.ExpressionPresenter; +import ru.neurospb.calculator.ui.presenters.calculator.ICalculator; + +/** + * Точка Старта приложения. + * + * @author Алексей Утовка + * @version а1 + */ + +public class StartPointActivity extends AppCompatActivity { + private static volatile StartPointActivity singletonActivity; + private ICalculator.ExpressionPresenter calculatorExpressionPresenter; + + //ACTIVITY LIFECYCLE CALLBACKS + //OnCreate + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + singletonActivity = this; + prepareUI(); + calculatorExpressionPresenter = new ExpressionPresenter(); + calculatorExpressionPresenter.loadState(savedInstanceState); + } + //OnStart / OnRestart + //OnResume + //OnPause + //OnStop + //OnDestroy + protected void onDestroy() { + singletonActivity = null; + calculatorExpressionPresenter = null; + super.onDestroy(); + } + + //ACTIVITY CALLBACKS + //onSaveInstanceState + @Override + protected void onSaveInstanceState(Bundle state) { + calculatorExpressionPresenter.saveState(state); + super.onSaveInstanceState(state); + } + + //МЕТОДЫ КЛАССА + /** + * Базовые настройки UI приложения + */ + private void prepareUI() { + if (getSupportActionBar() != null) getSupportActionBar().hide(); + setContentView(R.layout.wrapper); + } + /** + * Возвращает синглтон себя. Если не был инициализирован, + * тогда инициализирует и возвращает себя. + */ + public static StartPointActivity getInstance() { + if (singletonActivity == null)//just for lint. Has to be instantiated in runtime already. + singletonActivity = new StartPointActivity(); + return singletonActivity; + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/views/calculator/CalculatorView.java b/app/src/main/java/ru/neurospb/calculator/ui/views/calculator/CalculatorView.java new file mode 100644 index 0000000..ba6cbb5 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/views/calculator/CalculatorView.java @@ -0,0 +1,71 @@ +package ru.neurospb.calculator.ui.views.calculator; + +import android.support.v7.widget.AppCompatTextView; +import android.view.View; +import android.widget.HorizontalScrollView; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.ui.presenters.calculator.ICalculator; +import ru.neurospb.calculator.ui.views.base.AbstractView; +import ru.neurospb.calculator.ui.views.base.StartPointActivity; + +import static android.view.View.FOCUS_RIGHT; + +/** + * ICalculator.View. + * + * @author Алексей Утовка + * @version а1 + */ + +public class CalculatorView extends AbstractView implements ICalculator.View { + private AppCompatTextView expressionTextView; + + //КОНСТРУКТОР + public CalculatorView() { + super(R.layout.calculator); + expressionTextView = StartPointActivity.getInstance().findViewById(R.id.expressionText); + bindKeyboard(); + } + + //ИМПЛЕМЕНТАЦИЯ: ICalculator.View + @Override + public void showExpression(String expressionString) { + expressionTextView.setText(expressionString); + final HorizontalScrollView scroller = (HorizontalScrollView) expressionTextView.getParent(); + scroller.post(new Runnable() { + @Override + public void run() { + scroller.fullScroll(FOCUS_RIGHT); + } + }); + } + + //МЕТОДЫ КЛАССА + /** + * Подготовка клавиатуры + */ + private void bindKeyboard() { + View.OnClickListener keyboardListener = new SoftKeyboardListener(); + StartPointActivity activity = StartPointActivity.getInstance(); + activity.findViewById(R.id.button0).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button1).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button2).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button3).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button4).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button5).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button6).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button7).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button8).setOnClickListener(keyboardListener); + activity.findViewById(R.id.button9).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonPoint).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonDivide).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonMultiply).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonSubtract).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonAdd).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonOpenBracket).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonCloseBracket).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonDelete).setOnClickListener(keyboardListener); + activity.findViewById(R.id.buttonEqual).setOnClickListener(keyboardListener); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/neurospb/calculator/ui/views/calculator/SoftKeyboardListener.java b/app/src/main/java/ru/neurospb/calculator/ui/views/calculator/SoftKeyboardListener.java new file mode 100644 index 0000000..cdddfb0 --- /dev/null +++ b/app/src/main/java/ru/neurospb/calculator/ui/views/calculator/SoftKeyboardListener.java @@ -0,0 +1,43 @@ +package ru.neurospb.calculator.ui.views.calculator; + +import android.view.View; + +import ru.neurospb.calculator.R; +import ru.neurospb.calculator.ui.models.KeyboardInputModel; +import ru.neurospb.calculator.ui.presenters.calculator.CommandHandler; +import ru.neurospb.calculator.ui.presenters.calculator.ICalculator; +import ru.neurospb.calculator.ui.presenters.calculator.InputValidator; + +/** + * Слушатель нажатий на кнопки софт-клавиатуры калькулятора. + * Ловит нажатие кнопки и передаёт событие валидатору пользовательского ввода/обработчику пользовательских команд. + * + * @author Алексей Утовка + * @version а1 + */ + +public class SoftKeyboardListener implements View.OnClickListener { + private ICalculator.InputValidator validator; + private ICalculator.CommandHandler commandHandler; + + //КОНСТРУКТОР + SoftKeyboardListener() { + validator = new InputValidator(); + commandHandler = new CommandHandler(); + } + + //ИМПЛЕМЕНТАЦИЯ: View.OnClickListener + @Override + public void onClick(View v) { + int viewId = v.getId(); + switch (viewId) { + default: + validator.validateInput(new KeyboardInputModel(viewId)); + break; + case R.id.buttonDelete: + case R.id.buttonEqual: + commandHandler.onCommand(viewId); + break; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/app_icon.png b/app/src/main/res/drawable/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ac911e9870d11cfdfaafafcb8544b82d164862 GIT binary patch literal 14191 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/calculator.xml b/app/src/main/res/layout/calculator.xml new file mode 100644 index 0000000..2acb760 --- /dev/null +++ b/app/src/main/res/layout/calculator.xml @@ -0,0 +1,358 @@ + + + + + + + + + + + + +