/*
* Copyright 2013 serso aka se.solovyev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Contact details
*
* Email: se.solovyev@gmail.com
* Site: http://se.solovyev.org
*/
package org.solovyev.android.calculator;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.text.TextUtils;
import com.squareup.otto.Bus;
import org.solovyev.android.Check;
import org.solovyev.android.calculator.functions.FunctionsRegistry;
import org.solovyev.android.calculator.math.MathType;
import org.solovyev.android.calculator.operators.OperatorsRegistry;
import org.solovyev.android.calculator.operators.PostfixFunctionsRegistry;
import org.solovyev.android.calculator.preferences.PreferenceEntry;
import org.solovyev.android.prefs.IntegerPreference;
import org.solovyev.android.prefs.Preference;
import org.solovyev.android.prefs.StringPreference;
import org.solovyev.common.NumberFormatter;
import org.solovyev.common.text.CharacterMapper;
import org.solovyev.common.text.EnumMapper;
import org.solovyev.common.text.NumberMapper;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import jscl.AngleUnit;
import jscl.JsclMathEngine;
import jscl.MathEngine;
import jscl.NumeralBase;
import jscl.math.operator.Operator;
import jscl.text.Identifier;
import jscl.text.Parser;
import midpcalc.Real;
@Singleton
public class Engine implements SharedPreferences.OnSharedPreferenceChangeListener {
@Nonnull
private final MathEngine mathEngine;
@Inject
SharedPreferences preferences;
@Inject
Bus bus;
@Inject
ErrorReporter errorReporter;
@Inject
FunctionsRegistry functionsRegistry;
@Inject
VariablesRegistry variablesRegistry;
@Inject
OperatorsRegistry operatorsRegistry;
@Inject
PostfixFunctionsRegistry postfixFunctionsRegistry;
@Nonnull
private String multiplicationSign = Preferences.multiplicationSign.getDefaultValue();
public Engine(@Nonnull MathEngine mathEngine, @Nonnull VariablesRegistry variablesRegistry, @Nonnull FunctionsRegistry functionsRegistry, @Nonnull OperatorsRegistry operatorsRegistry, @Nonnull PostfixFunctionsRegistry postfixFunctionsRegistry) {
this.mathEngine = mathEngine;
this.variablesRegistry = variablesRegistry;
this.functionsRegistry = functionsRegistry;
this.operatorsRegistry = operatorsRegistry;
this.postfixFunctionsRegistry = postfixFunctionsRegistry;
}
@Inject
public Engine(@Nonnull JsclMathEngine mathEngine) {
this.mathEngine = mathEngine;
this.mathEngine.setPrecision(5);
this.mathEngine.setGroupingSeparator(JsclMathEngine.GROUPING_SEPARATOR_DEFAULT);
}
private static void migratePreference(@Nonnull SharedPreferences preferences, @Nonnull StringPreference<?> preference, @Nonnull String oldKey, @Nonnull SharedPreferences.Editor editor) {
if (!preferences.contains(oldKey)) {
return;
}
editor.putString(preference.getKey(), preferences.getString(oldKey, null));
}
public static boolean isValidName(@Nullable String name) {
if (!TextUtils.isEmpty(name)) {
try {
final String parsed = Identifier.parser.parse(Parser.Parameters.get(name), null);
return TextUtils.equals(parsed, name);
} catch (jscl.text.ParseException e) {
// not valid name;
}
}
return false;
}
@Nonnull
public VariablesRegistry getVariablesRegistry() {
return variablesRegistry;
}
@Nonnull
public FunctionsRegistry getFunctionsRegistry() {
return functionsRegistry;
}
@Nonnull
public EntitiesRegistry<Operator> getOperatorsRegistry() {
return operatorsRegistry;
}
@Nonnull
public EntitiesRegistry<Operator> getPostfixFunctionsRegistry() {
return postfixFunctionsRegistry;
}
@Nonnull
public MathEngine getMathEngine() {
return mathEngine;
}
public void init(@Nonnull Executor initThread) {
Check.isMainThread();
checkPreferences();
preferences.registerOnSharedPreferenceChangeListener(this);
applyPreferences();
initThread.execute(new Runnable() {
@Override
public void run() {
initAsync();
}
});
}
private void initPreferences(SharedPreferences.Editor editor) {
if (!Engine.Preferences.Output.separator.isSet(preferences)) {
final Locale locale = Locale.getDefault();
if (locale != null) {
final DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(locale);
final int index = MathType.grouping_separator.getTokens().indexOf(String.valueOf(decimalFormatSymbols.getGroupingSeparator()));
final char separator;
if (index >= 0) {
separator = MathType.grouping_separator.getTokens().get(index).charAt(0);
} else {
separator = JsclMathEngine.GROUPING_SEPARATOR_DEFAULT;
}
Engine.Preferences.Output.separator.putPreference(editor, separator);
}
}
Engine.Preferences.angleUnit.tryPutDefault(preferences, editor);
Engine.Preferences.numeralBase.tryPutDefault(preferences, editor);
Preferences.Output.notation.tryPutDefault(preferences, editor);
Preferences.Output.separator.tryPutDefault(preferences, editor);
Preferences.Output.precision.tryPutDefault(preferences, editor);
}
private void checkPreferences() {
final int oldVersion;
if (Preferences.version.isSet(preferences)) {
oldVersion = Preferences.version.getPreference(preferences);
} else {
oldVersion = 0;
}
final int newVersion = Preferences.version.getDefaultValue();
if (oldVersion == newVersion) {
return;
}
final SharedPreferences.Editor editor = preferences.edit();
if (oldVersion == 0) {
migratePreference(preferences, Preferences.Output.separator, "org.solovyev.android.calculator.CalculatorActivity_calc_grouping_separator", editor);
migratePreference(preferences, Preferences.multiplicationSign, "org.solovyev.android.calculator.CalculatorActivity_calc_multiplication_sign", editor);
migratePreference(preferences, Preferences.numeralBase, "org.solovyev.android.calculator.CalculatorActivity_numeral_bases", editor);
migratePreference(preferences, Preferences.angleUnit, "org.solovyev.android.calculator.CalculatorActivity_angle_units", editor);
migratePreference(preferences, Preferences.Output.precision, "org.solovyev.android.calculator.CalculatorModel_result_precision", editor);
if (preferences.contains("engine.output.science_notation")) {
final boolean scientific = preferences.getBoolean("engine.output.science_notation", false);
Preferences.Output.notation.putPreference(editor, scientific ? Notation.sci : Notation.dec);
}
if (preferences.contains("org.solovyev.android.calculator.CalculatorModel_round_result")) {
final boolean round = preferences.getBoolean("org.solovyev.android.calculator.CalculatorModel_round_result", true);
if (!round) {
Preferences.Output.precision.putPreference(editor, NumberFormatter.MAX_PRECISION);
}
}
// #initPreferences rely on all changes to be committed
editor.apply();
initPreferences(editor);
} else if (oldVersion == 1) {
migratePreference(preferences, Preferences.Output.separator, "engine.groupingSeparator", editor);
if (preferences.contains("engine.output.scientificNotation")) {
final boolean scientific = preferences.getBoolean("engine.output.scientificNotation", false);
Preferences.Output.notation.putPreference(editor, scientific ? Notation.sci : Notation.dec);
}
if (preferences.contains("engine.output.round")) {
final boolean round = preferences.getBoolean("engine.output.round", true);
if (!round) {
Preferences.Output.precision.putPreference(editor, NumberFormatter.MAX_PRECISION);
}
}
// #initPreferences rely on all changes to be committed
editor.apply();
// preferences should be initialized again as:
// 1. It was forgotten for 0 version
// 2. There is a bunch of new preferences
initPreferences(editor);
}
Preferences.version.putDefault(editor);
editor.apply();
}
private void initAsync() {
init(variablesRegistry);
init(functionsRegistry);
init(operatorsRegistry);
init(postfixFunctionsRegistry);
}
private void init(@Nonnull EntitiesRegistry<?> registry) {
try {
registry.init();
} catch (Exception e) {
errorReporter.onException(e);
}
}
private void applyPreferences() {
Check.isMainThread();
mathEngine.setAngleUnits(Preferences.angleUnit.getPreference(preferences));
mathEngine.setNumeralBase(Preferences.numeralBase.getPreference(preferences));
setMultiplicationSign(Preferences.multiplicationSign.getPreference(preferences));
mathEngine.setPrecision(Preferences.Output.precision.getPreference(preferences));
mathEngine.setNotation(Preferences.Output.notation.getPreference(preferences).id);
mathEngine.setGroupingSeparator(Preferences.Output.separator.getPreference(preferences));
bus.post(ChangedEvent.INSTANCE);
}
@Nonnull
public String getMultiplicationSign() {
return this.multiplicationSign;
}
public void setMultiplicationSign(@Nonnull String multiplicationSign) {
this.multiplicationSign = multiplicationSign;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (Preferences.getPreferenceKeys().contains(key)) {
applyPreferences();
}
}
public enum Notation implements PreferenceEntry {
dec(Real.NumberFormat.FSE_NONE, R.string.cpp_number_format_dec),
eng(Real.NumberFormat.FSE_ENG, R.string.cpp_number_format_eng),
sci(Real.NumberFormat.FSE_SCI, R.string.cpp_number_format_sci);
public final int id;
@StringRes
public final int name;
Notation(int id, @StringRes int name) {
this.id = id;
this.name = name;
}
@NonNull
@Override
public CharSequence getName(@NonNull Context context) {
return context.getString(name);
}
@NonNull
@Override
public CharSequence getId() {
return name();
}
}
public static class ChangedEvent {
static final ChangedEvent INSTANCE = new ChangedEvent();
private ChangedEvent() {
}
}
public static class Preferences {
public static final StringPreference<String> multiplicationSign = StringPreference.of("engine.multiplicationSign", "×");
public static final StringPreference<NumeralBase> numeralBase = StringPreference.ofTypedValue("engine.numeralBase", "dec", EnumMapper.of(NumeralBase.class));
public static final StringPreference<AngleUnit> angleUnit = StringPreference.ofTypedValue("engine.angleUnit", "deg", EnumMapper.of(AngleUnit.class));
public static final Preference<Integer> version = IntegerPreference.of("engine.version", 2);
private static final List<String> preferenceKeys = new ArrayList<>();
static {
preferenceKeys.add(multiplicationSign.getKey());
preferenceKeys.add(numeralBase.getKey());
preferenceKeys.add(angleUnit.getKey());
preferenceKeys.add(Output.precision.getKey());
preferenceKeys.add(Output.notation.getKey());
preferenceKeys.add(Output.separator.getKey());
}
@Nonnull
public static List<String> getPreferenceKeys() {
return Collections.unmodifiableList(preferenceKeys);
}
@StringRes
public static int angleUnitName(AngleUnit angleUnit) {
switch (angleUnit) {
case deg:
return R.string.cpp_deg;
case rad:
return R.string.cpp_rad;
case grad:
return R.string.cpp_grad;
case turns:
return R.string.cpp_turns;
}
return 0;
}
@StringRes
public static int numeralBaseName(NumeralBase numeralBase) {
switch (numeralBase) {
case bin:
return R.string.cpp_bin;
case oct:
return R.string.cpp_oct;
case dec:
return R.string.cpp_dec;
case hex:
return R.string.cpp_hex;
}
return 0;
}
public static class Output {
public static final StringPreference<Integer> precision = StringPreference.ofTypedValue("engine.output.precision", "5", NumberMapper.of(Integer.class));
public static final StringPreference<Notation> notation = StringPreference.ofEnum("engine.output.notation", Notation.dec, Notation.class);
public static final StringPreference<Character> separator = StringPreference.ofTypedValue("engine.output.separator", JsclMathEngine.GROUPING_SEPARATOR_DEFAULT, CharacterMapper.INSTANCE);
}
}
}