highlighting in editor + refactor
This commit is contained in:
parent
454789a408
commit
6b120465b5
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<LinearLayout a:layout_weight="2" a:layout_width="match_parent" a:layout_height="0dp">
|
<LinearLayout a:layout_weight="2" a:layout_width="match_parent" a:layout_height="0dp">
|
||||||
|
|
||||||
<org.solovyev.android.calculator.CalculatorEditText
|
<org.solovyev.android.calculator.CalculatorEditor
|
||||||
a:id="@+id/editText"
|
a:id="@+id/editText"
|
||||||
style="@style/display_style"
|
style="@style/display_style"
|
||||||
a:inputType="textMultiLine"
|
a:inputType="textMultiLine"
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<LinearLayout a:layout_weight="2" a:layout_width="match_parent" a:layout_height="0dp">
|
<LinearLayout a:layout_weight="2" a:layout_width="match_parent" a:layout_height="0dp">
|
||||||
|
|
||||||
<org.solovyev.android.calculator.CalculatorEditText
|
<org.solovyev.android.calculator.CalculatorEditor
|
||||||
a:id="@+id/editText"
|
a:id="@+id/editText"
|
||||||
style="@style/display_style"
|
style="@style/display_style"
|
||||||
a:inputType="textMultiLine"
|
a:inputType="textMultiLine"
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
<EditText a:id="@+id/var_edit_name"
|
<EditText a:id="@+id/var_edit_name"
|
||||||
a:layout_width="match_parent"
|
a:layout_width="match_parent"
|
||||||
a:layout_height="wrap_content"
|
a:layout_height="wrap_content"
|
||||||
|
a:digits="abcdefghijklmnopqrstuvwxyz1234567890"
|
||||||
a:textSize="20dp">
|
a:textSize="20dp">
|
||||||
</EditText>
|
</EditText>
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<string name="c_paste">paste</string>
|
<string name="c_paste">paste</string>
|
||||||
<string name="c_vars">vars</string>
|
<string name="c_vars">vars</string>
|
||||||
|
|
||||||
<string name="c_calc_color_display_title">Color expressions</string>
|
<string name="c_calc_color_display_title">Highlight expressions</string>
|
||||||
<string name="p_calc_result_precision_title">Precision of result</string>
|
<string name="p_calc_result_precision_title">Precision of result</string>
|
||||||
<string name="c_exit">Exit</string>
|
<string name="c_exit">Exit</string>
|
||||||
<string name="c_add">Add</string>
|
<string name="c_add">Add</string>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<string name="c_var_removal_confirmation">Removal confirmation</string>
|
<string name="c_var_removal_confirmation">Removal confirmation</string>
|
||||||
<string name="c_var_removal_confirmation_question">Do you really want to delete \'%s\' variable?</string>
|
<string name="c_var_removal_confirmation_question">Do you really want to delete \'%s\' variable?</string>
|
||||||
<string name="c_var_name">Name</string>
|
<string name="c_var_name">Name</string>
|
||||||
<string name="c_var_value">value</string>
|
<string name="c_var_value">Value</string>
|
||||||
<string name="c_var_description">Description</string>
|
<string name="c_var_description">Description</string>
|
||||||
<string name="c_var_create_var">Create variable</string>
|
<string name="c_var_create_var">Create variable</string>
|
||||||
<string name="c_var_edit_var">Edit variable</string>
|
<string name="c_var_edit_var">Edit variable</string>
|
||||||
|
@ -283,7 +283,7 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster, Sh
|
|||||||
|
|
||||||
final Boolean colorExpressionsInBracketsDefault = new BooleanMapper().parseValue(this.getString(R.string.p_calc_color_display));
|
final Boolean colorExpressionsInBracketsDefault = new BooleanMapper().parseValue(this.getString(R.string.p_calc_color_display));
|
||||||
assert colorExpressionsInBracketsDefault != null;
|
assert colorExpressionsInBracketsDefault != null;
|
||||||
this.calculatorView.getEditor().setHighlightExpressionInBrackets(sharedPreferences.getBoolean(this.getString(R.string.p_calc_color_display_key), colorExpressionsInBracketsDefault));
|
this.calculatorView.getEditor().setHighlightText(sharedPreferences.getBoolean(this.getString(R.string.p_calc_color_display_key), colorExpressionsInBracketsDefault));
|
||||||
|
|
||||||
this.calculatorView.evaluate();
|
this.calculatorView.evaluate();
|
||||||
}
|
}
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2009-2011. Created by serso aka se.solovyev.
|
|
||||||
* For more information, please, contact se.solovyev@gmail.com
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.solovyev.android.calculator;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.ColorStateList;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.solovyev.android.calculator.math.MathEntityType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User: serso
|
|
||||||
* Date: 9/17/11
|
|
||||||
* Time: 12:25 AM
|
|
||||||
*/
|
|
||||||
public class CalculatorEditText extends EditText {
|
|
||||||
|
|
||||||
private boolean highlightExpressionInBrackets = true;
|
|
||||||
|
|
||||||
public CalculatorEditText(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CalculatorEditText(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CalculatorEditText(Context context, AttributeSet attrs, int defStyle) {
|
|
||||||
super(context, attrs, defStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTextColor(ColorStateList colors) {
|
|
||||||
super.setTextColor(colors); //To change body of overridden methods use File | Settings | File Templates.
|
|
||||||
}
|
|
||||||
|
|
||||||
public void redraw() {
|
|
||||||
String text = getText().toString();
|
|
||||||
|
|
||||||
int selectionStart = getSelectionStart();
|
|
||||||
int selectionEnd = getSelectionEnd();
|
|
||||||
|
|
||||||
if (highlightExpressionInBrackets) {
|
|
||||||
|
|
||||||
int maxNumberOfOpenGroupSymbols = 0;
|
|
||||||
int numberOfOpenGroupSymbols = 0;
|
|
||||||
for (int i = 0; i < text.length(); i++) {
|
|
||||||
char ch = text.charAt(i);
|
|
||||||
if (MathEntityType.openGroupSymbols.contains(ch)) {
|
|
||||||
numberOfOpenGroupSymbols++;
|
|
||||||
maxNumberOfOpenGroupSymbols = Math.max(maxNumberOfOpenGroupSymbols, numberOfOpenGroupSymbols);
|
|
||||||
} else if (MathEntityType.closeGroupSymbols.contains(ch)) {
|
|
||||||
numberOfOpenGroupSymbols--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxNumberOfOpenGroupSymbols > 0) {
|
|
||||||
|
|
||||||
final StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
processGroup(sb, text, 0, 0, maxNumberOfOpenGroupSymbols);
|
|
||||||
|
|
||||||
Log.d(CalculatorEditText.class.getName(), sb.toString());
|
|
||||||
|
|
||||||
super.setText(Html.fromHtml(sb.toString()), BufferType.EDITABLE);
|
|
||||||
} else {
|
|
||||||
super.setText(text, BufferType.EDITABLE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.setText(text, BufferType.EDITABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelection(selectionStart, selectionEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int processGroup(@NotNull StringBuilder result, @NotNull String s, int i, int numberOfOpenings, int maxNumberOfGroups) {
|
|
||||||
|
|
||||||
result.append("<font color=\"").append(getColor(maxNumberOfGroups, numberOfOpenings)).append("\">");
|
|
||||||
|
|
||||||
for (; i < s.length(); i++) {
|
|
||||||
char ch = s.charAt(i);
|
|
||||||
|
|
||||||
if (MathEntityType.openGroupSymbols.contains(ch)) {
|
|
||||||
result.append(ch);
|
|
||||||
result.append("</font>");
|
|
||||||
i = processGroup(result, s, i + 1, numberOfOpenings + 1, maxNumberOfGroups);
|
|
||||||
result.append("<font color=\"").append(getColor(maxNumberOfGroups, numberOfOpenings)).append("\">");
|
|
||||||
if (i < s.length() && MathEntityType.closeGroupSymbols.contains(s.charAt(i))) {
|
|
||||||
result.append(s.charAt(i));
|
|
||||||
}
|
|
||||||
} else if (MathEntityType.closeGroupSymbols.contains(ch)) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
result.append(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append("</font>");
|
|
||||||
|
|
||||||
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getColor(int numberOfOpenGroupSymbols, int numberOfOpenings) {
|
|
||||||
final int baseColor = Color.WHITE;
|
|
||||||
|
|
||||||
double c = 1;
|
|
||||||
|
|
||||||
int i = ((int) (255 * c)) * numberOfOpenings / (numberOfOpenGroupSymbols + 1);
|
|
||||||
|
|
||||||
int result = Color.rgb(Color.red(baseColor) - i, Color.green(baseColor) - i, Color.blue(baseColor) - i);
|
|
||||||
|
|
||||||
return "#" + Integer.toHexString(result).substring(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isHighlightExpressionInBrackets() {
|
|
||||||
return highlightExpressionInBrackets;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHighlightExpressionInBrackets(boolean highlightExpressionInBrackets) {
|
|
||||||
this.highlightExpressionInBrackets = highlightExpressionInBrackets;
|
|
||||||
redraw();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2009-2011. Created by serso aka se.solovyev.
|
||||||
|
* For more information, please, contact se.solovyev@gmail.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.solovyev.android.calculator;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.solovyev.android.calculator.math.MathEntityType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User: serso
|
||||||
|
* Date: 9/17/11
|
||||||
|
* Time: 12:25 AM
|
||||||
|
*/
|
||||||
|
public class CalculatorEditor extends EditText {
|
||||||
|
|
||||||
|
private boolean highlightText = true;
|
||||||
|
|
||||||
|
public CalculatorEditor(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CalculatorEditor(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CalculatorEditor(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void redraw() {
|
||||||
|
String text = getText().toString();
|
||||||
|
|
||||||
|
int selectionStart = getSelectionStart();
|
||||||
|
int selectionEnd = getSelectionEnd();
|
||||||
|
|
||||||
|
if (highlightText) {
|
||||||
|
|
||||||
|
text = highlightText(text);
|
||||||
|
|
||||||
|
super.setText(Html.fromHtml(text), BufferType.EDITABLE);
|
||||||
|
} else {
|
||||||
|
super.setText(text, BufferType.EDITABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelection(selectionStart, selectionEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String highlightText(@NotNull final String text) {
|
||||||
|
final String result;
|
||||||
|
|
||||||
|
int maxNumberOfOpenGroupSymbols = 0;
|
||||||
|
int numberOfOpenGroupSymbols = 0;
|
||||||
|
|
||||||
|
final StringBuilder text1 = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
final MathEntityType.Result mathType = MathEntityType.getType(text, i);
|
||||||
|
|
||||||
|
switch (mathType.getMathEntityType()) {
|
||||||
|
case open_group_symbol:
|
||||||
|
numberOfOpenGroupSymbols++;
|
||||||
|
maxNumberOfOpenGroupSymbols = Math.max(maxNumberOfOpenGroupSymbols, numberOfOpenGroupSymbols);
|
||||||
|
text1.append(text.charAt(i));
|
||||||
|
break;
|
||||||
|
case close_group_symbol:
|
||||||
|
numberOfOpenGroupSymbols--;
|
||||||
|
text1.append(text.charAt(i));
|
||||||
|
break;
|
||||||
|
case function:
|
||||||
|
i = processHighlightedText(text1, i, mathType.getS(), "i");
|
||||||
|
break;
|
||||||
|
case constant:
|
||||||
|
i = processHighlightedText(text1, i, mathType.getS(), "b");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
text1.append(text.charAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxNumberOfOpenGroupSymbols > 0) {
|
||||||
|
|
||||||
|
final StringBuilder text2 = new StringBuilder();
|
||||||
|
|
||||||
|
processBracketGroup(text2, text1.toString(), 0, 0, maxNumberOfOpenGroupSymbols);
|
||||||
|
|
||||||
|
Log.d(CalculatorEditor.class.getName(), text2.toString());
|
||||||
|
|
||||||
|
result = text2.toString();
|
||||||
|
} else {
|
||||||
|
result = text1.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int processHighlightedText(@NotNull StringBuilder result, int i, @NotNull String functionName, @NotNull String tag) {
|
||||||
|
result.append("<").append(tag).append(">").append(functionName).append("</").append(tag).append(">");
|
||||||
|
return i + functionName.length() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int processBracketGroup(@NotNull StringBuilder result, @NotNull String s, int i, int numberOfOpenings, int maxNumberOfGroups) {
|
||||||
|
|
||||||
|
result.append("<font color=\"").append(getColor(maxNumberOfGroups, numberOfOpenings)).append("\">");
|
||||||
|
|
||||||
|
for (; i < s.length(); i++) {
|
||||||
|
char ch = s.charAt(i);
|
||||||
|
|
||||||
|
if (MathEntityType.openGroupSymbols.contains(ch)) {
|
||||||
|
result.append(ch);
|
||||||
|
result.append("</font>");
|
||||||
|
i = processBracketGroup(result, s, i + 1, numberOfOpenings + 1, maxNumberOfGroups);
|
||||||
|
result.append("<font color=\"").append(getColor(maxNumberOfGroups, numberOfOpenings)).append("\">");
|
||||||
|
if (i < s.length() && MathEntityType.closeGroupSymbols.contains(s.charAt(i))) {
|
||||||
|
result.append(s.charAt(i));
|
||||||
|
}
|
||||||
|
} else if (MathEntityType.closeGroupSymbols.contains(ch)) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
result.append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append("</font>");
|
||||||
|
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getColor(int numberOfOpenGroupSymbols, int numberOfOpenings) {
|
||||||
|
final int baseColor = Color.WHITE;
|
||||||
|
|
||||||
|
double c = 1;
|
||||||
|
|
||||||
|
int i = ((int) (255 * c)) * numberOfOpenings / (numberOfOpenGroupSymbols + 1);
|
||||||
|
|
||||||
|
int result = Color.rgb(Color.red(baseColor) - i, Color.green(baseColor) - i, Color.blue(baseColor) - i);
|
||||||
|
|
||||||
|
return "#" + Integer.toHexString(result).substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHighlightText() {
|
||||||
|
return highlightText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHighlightText(boolean highlightText) {
|
||||||
|
this.highlightText = highlightText;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
}
|
@ -41,7 +41,7 @@ public class CalculatorModel {
|
|||||||
|
|
||||||
private static CalculatorModel instance;
|
private static CalculatorModel instance;
|
||||||
|
|
||||||
private CalculatorModel(@Nullable Context context) {
|
public CalculatorModel(@Nullable Context context) {
|
||||||
load(context);
|
load(context);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
@ -16,6 +16,7 @@ import android.view.*;
|
|||||||
import android.widget.*;
|
import android.widget.*;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.solovyev.android.calculator.math.MathEntityType;
|
||||||
import org.solovyev.common.utils.StringUtils;
|
import org.solovyev.common.utils.StringUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -150,21 +151,26 @@ public class CalculatorVarsActivity extends ListActivity {
|
|||||||
if (!StringUtils.isEmpty(name)) {
|
if (!StringUtils.isEmpty(name)) {
|
||||||
final Var varFromRegister = varsRegister.getVar(name);
|
final Var varFromRegister = varsRegister.getVar(name);
|
||||||
if (varFromRegister == null || varFromRegister == editedInstance) {
|
if (varFromRegister == null || varFromRegister == editedInstance) {
|
||||||
|
final MathEntityType.Result mathType = MathEntityType.getType(name, 0);
|
||||||
|
|
||||||
boolean correctDouble = true;
|
if (mathType.getMathEntityType() == MathEntityType.text || mathType.getMathEntityType() == MathEntityType.constant) {
|
||||||
try {
|
boolean correctDouble = true;
|
||||||
Double.valueOf(value);
|
try {
|
||||||
} catch (NumberFormatException e) {
|
Double.valueOf(value);
|
||||||
correctDouble = false;
|
} catch (NumberFormatException e) {
|
||||||
}
|
correctDouble = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (correctDouble) {
|
if (correctDouble) {
|
||||||
varBuilder.setName(name);
|
varBuilder.setName(name);
|
||||||
varBuilder.setValue(value);
|
varBuilder.setValue(value);
|
||||||
varBuilder.setDescription(description);
|
varBuilder.setDescription(description);
|
||||||
error = null;
|
error = null;
|
||||||
|
} else {
|
||||||
|
error = "Value is not a number!";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error = "Value is not a number!";
|
error = "Variable name clashes with function name!";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error = "Variable with same name already exist!";
|
error = "Variable with same name already exist!";
|
||||||
|
@ -39,7 +39,7 @@ public class CalculatorView implements CursorControl, HistoryControl<CalculatorH
|
|||||||
public static final int EVAL_DELAY_MILLIS = 500;
|
public static final int EVAL_DELAY_MILLIS = 500;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private final CalculatorEditText editor;
|
private final CalculatorEditor editor;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private final CalculatorDisplay display;
|
private final CalculatorDisplay display;
|
||||||
@ -59,7 +59,7 @@ public class CalculatorView implements CursorControl, HistoryControl<CalculatorH
|
|||||||
|
|
||||||
final InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
final InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
|
||||||
this.editor = (CalculatorEditText) activity.findViewById(R.id.editText);
|
this.editor = (CalculatorEditor) activity.findViewById(R.id.editText);
|
||||||
this.editor.setInputType(InputType.TYPE_NULL);
|
this.editor.setInputType(InputType.TYPE_NULL);
|
||||||
imm.hideSoftInputFromWindow(this.editor.getWindowToken(), 0);
|
imm.hideSoftInputFromWindow(this.editor.getWindowToken(), 0);
|
||||||
|
|
||||||
@ -192,24 +192,18 @@ public class CalculatorView implements CursorControl, HistoryControl<CalculatorH
|
|||||||
@Override
|
@Override
|
||||||
public void doOperation(@NotNull EditText editor) {
|
public void doOperation(@NotNull EditText editor) {
|
||||||
|
|
||||||
final MathEntityType type = MathEntityType.getType(text, 0);
|
final MathEntityType.Result mathType = MathEntityType.getType(text, 0);
|
||||||
|
|
||||||
int cursorPositionOffset = 0;
|
int cursorPositionOffset = 0;
|
||||||
final StringBuilder textToBeInserted = new StringBuilder(text);
|
final StringBuilder textToBeInserted = new StringBuilder(text);
|
||||||
if (type != null) {
|
switch (mathType.getMathEntityType()) {
|
||||||
switch (type) {
|
case function:
|
||||||
case function:
|
textToBeInserted.append("()");
|
||||||
textToBeInserted.append("()");
|
cursorPositionOffset = -1;
|
||||||
cursorPositionOffset = -1;
|
break;
|
||||||
break;
|
case group_symbols:
|
||||||
case group_symbols:
|
cursorPositionOffset = -1;
|
||||||
cursorPositionOffset = -1;
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.getText().insert(editor.getSelectionStart(), textToBeInserted.toString());
|
editor.getText().insert(editor.getSelectionStart(), textToBeInserted.toString());
|
||||||
@ -287,7 +281,7 @@ public class CalculatorView implements CursorControl, HistoryControl<CalculatorH
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public CalculatorEditText getEditor() {
|
public CalculatorEditor getEditor() {
|
||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,35 +23,28 @@ public class ToJsclPreprocessor implements Preprocessor {
|
|||||||
final StartsWithFinder startsWithFinder = new StartsWithFinder(s, 0);
|
final StartsWithFinder startsWithFinder = new StartsWithFinder(s, 0);
|
||||||
final StringBuilder sb = new StringBuilder();
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
boolean constantBefore = false;
|
MathEntityType.Result mathTypeResult = null;
|
||||||
for (int i = 0; i < s.length(); i++) {
|
for (int i = 0; i < s.length(); i++) {
|
||||||
char ch = s.charAt(i);
|
char ch = s.charAt(i);
|
||||||
startsWithFinder.setI(i);
|
startsWithFinder.setI(i);
|
||||||
|
|
||||||
checkMultiplicationSignBeforeFunction(sb, s, i, constantBefore);
|
mathTypeResult = checkMultiplicationSignBeforeFunction(sb, s, i, mathTypeResult);
|
||||||
constantBefore = false;
|
|
||||||
|
|
||||||
if (MathEntityType.openGroupSymbols.contains(ch)) {
|
final MathEntityType mathType = mathTypeResult.getMathEntityType();
|
||||||
|
if (mathType == MathEntityType.open_group_symbol) {
|
||||||
sb.append('(');
|
sb.append('(');
|
||||||
} else if (MathEntityType.closeGroupSymbols.contains(ch)) {
|
} else if (mathType == MathEntityType.close_group_symbol) {
|
||||||
sb.append(')');
|
sb.append(')');
|
||||||
} else if (ch == '×' || ch == '∙') {
|
} else if (ch == '×' || ch == '∙') {
|
||||||
sb.append("*");
|
sb.append("*");
|
||||||
|
} else if ( mathType == MathEntityType.function ){
|
||||||
|
sb.append(toJsclFunction(mathTypeResult.getS()));
|
||||||
|
i += mathTypeResult.getS().length() - 1;
|
||||||
|
} else if ( mathType == MathEntityType.constant ) {
|
||||||
|
sb.append(mathTypeResult.getS());
|
||||||
|
i += mathTypeResult.getS().length() - 1;
|
||||||
} else {
|
} else {
|
||||||
String entity = CollectionsUtils.get(MathEntityType.prefixFunctions, startsWithFinder);
|
sb.append(ch);
|
||||||
if (entity == null) {
|
|
||||||
entity = CollectionsUtils.get(CalculatorModel.getInstance().getVarsRegister().getVarNames(), startsWithFinder);
|
|
||||||
if (entity == null) {
|
|
||||||
sb.append(ch);
|
|
||||||
} else {
|
|
||||||
sb.append(entity);
|
|
||||||
i += entity.length() - 1;
|
|
||||||
constantBefore = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sb.append(toJsclFunction(entity));
|
|
||||||
i += entity.length() - 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,18 +103,14 @@ public class ToJsclPreprocessor implements Preprocessor {
|
|||||||
int result = position;
|
int result = position;
|
||||||
for (; result >= 0; result--) {
|
for (; result >= 0; result--) {
|
||||||
|
|
||||||
final MathEntityType mathEntityType = MathEntityType.getType(s, result);
|
final MathEntityType mathEntityType = MathEntityType.getMathEntityType(s, result);
|
||||||
|
|
||||||
if (mathEntityType != null) {
|
if (CollectionsUtils.contains(mathEntityType, MathEntityType.digit, MathEntityType.dot)) {
|
||||||
if (CollectionsUtils.contains(mathEntityType, MathEntityType.digit, MathEntityType.dot)) {
|
// continue
|
||||||
// continue
|
} else if (mathEntityType == MathEntityType.close_group_symbol) {
|
||||||
} else if (MathEntityType.closeGroupSymbols.contains(s.charAt(result))) {
|
numberOfOpenGroups++;
|
||||||
numberOfOpenGroups++;
|
} else if (mathEntityType == MathEntityType.open_group_symbol) {
|
||||||
} else if (MathEntityType.openGroupSymbols.contains(s.charAt(result))) {
|
numberOfOpenGroups--;
|
||||||
numberOfOpenGroups--;
|
|
||||||
} else {
|
|
||||||
if (stop(s, numberOfOpenGroups, result)) break;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (stop(s, numberOfOpenGroups, result)) break;
|
if (stop(s, numberOfOpenGroups, result)) break;
|
||||||
}
|
}
|
||||||
@ -136,8 +125,8 @@ public class ToJsclPreprocessor implements Preprocessor {
|
|||||||
final EndsWithFinder endsWithFinder = new EndsWithFinder(s);
|
final EndsWithFinder endsWithFinder = new EndsWithFinder(s);
|
||||||
endsWithFinder.setI(i + 1);
|
endsWithFinder.setI(i + 1);
|
||||||
if (!CollectionsUtils.contains(MathEntityType.prefixFunctions, FilterType.included, endsWithFinder)) {
|
if (!CollectionsUtils.contains(MathEntityType.prefixFunctions, FilterType.included, endsWithFinder)) {
|
||||||
MathEntityType type = MathEntityType.getType(s, i);
|
MathEntityType type = MathEntityType.getMathEntityType(s, i);
|
||||||
if (type != null && type != MathEntityType.constant) {
|
if (type != MathEntityType.constant) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,25 +174,29 @@ public class ToJsclPreprocessor implements Preprocessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkMultiplicationSignBeforeFunction(@NotNull StringBuilder sb, @NotNull String s, int i, boolean constantBefore) {
|
@NotNull
|
||||||
|
private static MathEntityType.Result checkMultiplicationSignBeforeFunction(@NotNull StringBuilder sb,
|
||||||
|
@NotNull String s,
|
||||||
|
int i,
|
||||||
|
@Nullable MathEntityType.Result mathTypeBeforeResult) {
|
||||||
|
MathEntityType.Result result = MathEntityType.getType(s, i);
|
||||||
|
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
// get character before function
|
|
||||||
char chBefore = s.charAt(i - 1);
|
|
||||||
char ch = s.charAt(i);
|
|
||||||
|
|
||||||
final MathEntityType mathTypeBefore = MathEntityType.getType(String.valueOf(chBefore));
|
final MathEntityType mathType = result.getMathEntityType();
|
||||||
final MathEntityType mathType = MathEntityType.getType(s, i);
|
assert mathTypeBeforeResult != null;
|
||||||
|
final MathEntityType mathTypeBefore = mathTypeBeforeResult.getMathEntityType();
|
||||||
|
|
||||||
if (constantBefore || (mathTypeBefore != MathEntityType.binary_operation &&
|
if (mathTypeBefore == MathEntityType.constant || (mathTypeBefore != MathEntityType.binary_operation &&
|
||||||
mathTypeBefore != MathEntityType.unary_operation &&
|
mathTypeBefore != MathEntityType.unary_operation &&
|
||||||
mathTypeBefore != MathEntityType.function &&
|
mathTypeBefore != MathEntityType.function &&
|
||||||
!MathEntityType.openGroupSymbols.contains(chBefore))) {
|
mathTypeBefore != MathEntityType.open_group_symbol)) {
|
||||||
|
|
||||||
if (mathType == MathEntityType.constant) {
|
if (mathType == MathEntityType.constant) {
|
||||||
sb.append("*");
|
sb.append("*");
|
||||||
} else if (MathEntityType.openGroupSymbols.contains(ch) && mathTypeBefore != null) {
|
} else if (mathType == MathEntityType.open_group_symbol && mathTypeBefore != null) {
|
||||||
sb.append("*");
|
sb.append("*");
|
||||||
} else if (mathType == MathEntityType.digit && ((mathTypeBefore != MathEntityType.digit && mathTypeBefore != MathEntityType.dot) || constantBefore) ) {
|
} else if (mathType == MathEntityType.digit && ((mathTypeBefore != MathEntityType.digit && mathTypeBefore != MathEntityType.dot) || mathTypeBefore == MathEntityType.constant)) {
|
||||||
sb.append("*");
|
sb.append("*");
|
||||||
} else {
|
} else {
|
||||||
for (String function : MathEntityType.prefixFunctions) {
|
for (String function : MathEntityType.prefixFunctions) {
|
||||||
@ -215,6 +208,8 @@ public class ToJsclPreprocessor implements Preprocessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String wrap(@NotNull JsclOperation operation, @NotNull String s) {
|
public static String wrap(@NotNull JsclOperation operation, @NotNull String s) {
|
||||||
|
@ -6,19 +6,16 @@
|
|||||||
package org.solovyev.android.calculator.math;
|
package org.solovyev.android.calculator.math;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.solovyev.android.calculator.CalculatorModel;
|
import org.solovyev.android.calculator.CalculatorModel;
|
||||||
import org.solovyev.android.calculator.CharacterAtPositionFinder;
|
import org.solovyev.android.calculator.CharacterAtPositionFinder;
|
||||||
import org.solovyev.android.calculator.StartsWithFinder;
|
import org.solovyev.android.calculator.StartsWithFinder;
|
||||||
import org.solovyev.android.calculator.Var;
|
|
||||||
import org.solovyev.common.utils.CollectionsUtils;
|
|
||||||
import org.solovyev.common.utils.Finder;
|
import org.solovyev.common.utils.Finder;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.solovyev.common.utils.CollectionsUtils.get;
|
||||||
|
|
||||||
public enum MathEntityType {
|
public enum MathEntityType {
|
||||||
|
|
||||||
digit,
|
digit,
|
||||||
@ -29,11 +26,14 @@ public enum MathEntityType {
|
|||||||
unary_operation,
|
unary_operation,
|
||||||
binary_operation,
|
binary_operation,
|
||||||
group_symbols,
|
group_symbols,
|
||||||
group_symbol;
|
open_group_symbol,
|
||||||
|
close_group_symbol,
|
||||||
|
text;
|
||||||
|
|
||||||
public static final List<String> constants = Arrays.asList("e", "π", "i");
|
public static final List<String> constants = Arrays.asList("e", "π", "i");
|
||||||
|
|
||||||
public static final List<String> digits = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
public static final List<String> digits = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
||||||
|
|
||||||
public static final List<Character> dots = Arrays.asList('.');
|
public static final List<Character> dots = Arrays.asList('.');
|
||||||
|
|
||||||
public static final List<Character> unaryOperations = Arrays.asList('-', '=', '!');
|
public static final List<Character> unaryOperations = Arrays.asList('-', '=', '!');
|
||||||
@ -50,113 +50,114 @@ public enum MathEntityType {
|
|||||||
|
|
||||||
public static final List<Character> closeGroupSymbols = Arrays.asList(']', ')', '}');
|
public static final List<Character> closeGroupSymbols = Arrays.asList(']', ')', '}');
|
||||||
|
|
||||||
public static final List<Character> singleGroupSymbols;
|
/**
|
||||||
|
* Method determines mathematical entity type for text substring starting from ith index
|
||||||
static {
|
*
|
||||||
final List<Character> list = new ArrayList<Character>();
|
* @param text analyzed text
|
||||||
list.addAll(openGroupSymbols);
|
* @param i index which points to start of substring
|
||||||
list.addAll(closeGroupSymbols);
|
* @return math entity type of substring starting from ith index of specified text
|
||||||
singleGroupSymbols = Collections.unmodifiableList(list);
|
*/
|
||||||
|
@NotNull
|
||||||
|
public static MathEntityType getMathEntityType(@NotNull String text, int i) {
|
||||||
|
return getType(text, i).getMathEntityType();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NotNull
|
||||||
public static MathEntityType getType(@NotNull String s) {
|
public static Result getType(@NotNull String text, int i) {
|
||||||
MathEntityType result = null;
|
if (i < 0) {
|
||||||
|
throw new IllegalArgumentException("I must be more or equals to 0.");
|
||||||
if (s.length() == 1) {
|
} else if (i >= text.length() && i != 0) {
|
||||||
result = getType(s.charAt(0));
|
throw new IllegalArgumentException("I must be less than size of text.");
|
||||||
|
} else if (i == 0 && text.length() == 0) {
|
||||||
|
return new Result(MathEntityType.text, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null) {
|
final StartsWithFinder stringStartWithFinder = new StartsWithFinder(text, i);
|
||||||
if (prefixFunctions.contains(s)) {
|
final CharacterAtPositionFinder characterStartWithFinder = new CharacterAtPositionFinder(text, i);
|
||||||
result = MathEntityType.function;
|
|
||||||
} else if (isConstant(s)) {
|
String foundString = get(digits, stringStartWithFinder);
|
||||||
result = MathEntityType.constant;
|
if (foundString != null) {
|
||||||
} else if (groupSymbols.contains(s)) {
|
return new Result(MathEntityType.digit, foundString);
|
||||||
result = MathEntityType.group_symbols;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Character foundCharacter = get(dots, characterStartWithFinder);
|
||||||
return result;
|
if (foundCharacter != null) {
|
||||||
}
|
return new Result(dot, String.valueOf(foundCharacter));
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static MathEntityType getType(final char ch) {
|
|
||||||
MathEntityType result = null;
|
|
||||||
|
|
||||||
if (Character.isDigit(ch)) {
|
|
||||||
result = MathEntityType.digit;
|
|
||||||
} else if (postfixFunctions.contains(ch)) {
|
|
||||||
result = MathEntityType.postfix_function;
|
|
||||||
} else if (unaryOperations.contains(ch)) {
|
|
||||||
result = MathEntityType.unary_operation;
|
|
||||||
} else if (binaryOperations.contains(ch)) {
|
|
||||||
result = MathEntityType.binary_operation;
|
|
||||||
} else if (singleGroupSymbols.contains(ch)) {
|
|
||||||
result = MathEntityType.group_symbol;
|
|
||||||
} else if (isConstant(ch)) {
|
|
||||||
result = MathEntityType.constant;
|
|
||||||
} else if (dots.contains(ch)) {
|
|
||||||
result = MathEntityType.dot;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isConstant(final char ch) {
|
|
||||||
final String name = String.valueOf(ch);
|
|
||||||
|
|
||||||
return isConstant(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isConstant(final String name) {
|
|
||||||
return CollectionsUtils.get(CalculatorModel.getInstance().getVarsRegister().getVars(), new Finder<Var>() {
|
|
||||||
@Override
|
|
||||||
public boolean isFound(@Nullable Var var) {
|
|
||||||
return var != null && var.getName().equals(name);
|
|
||||||
}
|
|
||||||
}) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MathEntityType getType(String s, int i) {
|
|
||||||
final StartsWithFinder startsWithFinder = new StartsWithFinder(s, i);
|
|
||||||
final CharacterAtPositionFinder characterStartWithFinder = new CharacterAtPositionFinder(s, i);
|
|
||||||
|
|
||||||
return getType(startsWithFinder, characterStartWithFinder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static MathEntityType getType(@NotNull Finder<String> finder, @NotNull CharacterAtPositionFinder characterStartWithFinder) {
|
|
||||||
MathEntityType result = null;
|
|
||||||
|
|
||||||
if (contains(digits, finder)) {
|
|
||||||
result = MathEntityType.digit;
|
|
||||||
} else if (contains(postfixFunctions, characterStartWithFinder)) {
|
|
||||||
result = MathEntityType.postfix_function;
|
|
||||||
} else if (contains(unaryOperations, characterStartWithFinder)) {
|
|
||||||
result = MathEntityType.unary_operation;
|
|
||||||
} else if (contains(binaryOperations, characterStartWithFinder)) {
|
|
||||||
result = MathEntityType.binary_operation;
|
|
||||||
} else if (contains(groupSymbols, finder)) {
|
|
||||||
result = MathEntityType.group_symbols;
|
|
||||||
} else if (contains(singleGroupSymbols, characterStartWithFinder)) {
|
|
||||||
result = MathEntityType.group_symbol;
|
|
||||||
} else if (contains(prefixFunctions, finder)) {
|
|
||||||
result = MathEntityType.function;
|
|
||||||
} else if (contains(CalculatorModel.getInstance().getVarsRegister().getVarNames(), finder)) {
|
|
||||||
result = MathEntityType.constant;
|
|
||||||
} else if (contains(dots, characterStartWithFinder)) {
|
|
||||||
result = MathEntityType.dot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
foundCharacter = get(postfixFunctions, characterStartWithFinder);
|
||||||
|
if (foundCharacter != null) {
|
||||||
|
return new Result(postfix_function, String.valueOf(foundCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
foundCharacter = get(unaryOperations, characterStartWithFinder);
|
||||||
|
if (foundCharacter != null) {
|
||||||
|
return new Result(unary_operation, String.valueOf(foundCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
foundCharacter = get(binaryOperations, characterStartWithFinder);
|
||||||
|
if (foundCharacter != null) {
|
||||||
|
return new Result(binary_operation, String.valueOf(foundCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
foundString = get(groupSymbols, stringStartWithFinder);
|
||||||
|
if (foundString != null) {
|
||||||
|
return new Result(MathEntityType.group_symbols, foundString);
|
||||||
|
}
|
||||||
|
|
||||||
|
foundCharacter = get(openGroupSymbols, characterStartWithFinder);
|
||||||
|
if (foundCharacter != null) {
|
||||||
|
return new Result(open_group_symbol, String.valueOf(foundCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
foundCharacter = get(closeGroupSymbols, characterStartWithFinder);
|
||||||
|
if (foundCharacter != null) {
|
||||||
|
return new Result(close_group_symbol, String.valueOf(foundCharacter));
|
||||||
|
}
|
||||||
|
|
||||||
|
foundString = get(prefixFunctions, stringStartWithFinder);
|
||||||
|
if (foundString != null) {
|
||||||
|
return new Result(MathEntityType.function, foundString);
|
||||||
|
}
|
||||||
|
|
||||||
|
foundString = get(CalculatorModel.getInstance().getVarsRegister().getVarNames(), stringStartWithFinder);
|
||||||
|
if (foundString != null) {
|
||||||
|
return new Result(MathEntityType.constant, foundString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Result(MathEntityType.text, text.substring(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Result {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private final MathEntityType mathEntityType;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private final String s;
|
||||||
|
|
||||||
|
private Result(@NotNull MathEntityType mathEntityType, @NotNull String s){
|
||||||
|
this.mathEntityType = mathEntityType;
|
||||||
|
|
||||||
|
this.s = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String getS() {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public MathEntityType getMathEntityType() {
|
||||||
|
return mathEntityType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean contains(@NotNull List<String> list, @NotNull final Finder<String> startsWithFinder) {
|
private static boolean contains(@NotNull List<String> list, @NotNull final Finder<String> startsWithFinder) {
|
||||||
return CollectionsUtils.get(list, startsWithFinder) != null;
|
return get(list, startsWithFinder) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean contains(@NotNull List<Character> list, @NotNull final CharacterAtPositionFinder atPositionFinder) {
|
private static boolean contains(@NotNull List<Character> list, @NotNull final CharacterAtPositionFinder atPositionFinder) {
|
||||||
return CollectionsUtils.get(list, atPositionFinder) != null;
|
return get(list, atPositionFinder) != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2009-2011. Created by serso aka se.solovyev.
|
||||||
|
* For more information, please, contact se.solovyev@gmail.com
|
||||||
|
* or visit http://se.solovyev.org
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.solovyev.android.calculator.math;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.solovyev.android.calculator.CalculatorModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User: serso
|
||||||
|
* Date: 10/5/11
|
||||||
|
* Time: 1:25 AM
|
||||||
|
*/
|
||||||
|
public class MathEntityTypeTest {
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
CalculatorModel.init(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetType() throws Exception {
|
||||||
|
Assert.assertEquals(MathEntityType.function, MathEntityType.getType("sin", 0).getMathEntityType());
|
||||||
|
Assert.assertEquals(MathEntityType.text, MathEntityType.getType("sn", 0).getMathEntityType());
|
||||||
|
Assert.assertEquals(MathEntityType.text, MathEntityType.getType("s", 0).getMathEntityType());
|
||||||
|
Assert.assertEquals(MathEntityType.text, MathEntityType.getType("", 0).getMathEntityType());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Assert.assertEquals(MathEntityType.text, MathEntityType.getType("22", -1).getMathEntityType());
|
||||||
|
Assert.fail();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Assert.assertEquals(MathEntityType.text, MathEntityType.getType("22", 2).getMathEntityType());
|
||||||
|
Assert.fail();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user