Replace "," with ". " in TextWatcher - java

I am using this TextWatcher for .addtextOnChangeListener, the output of the string is ex: "123,456,77" i want it to be "123.456,77" if i use the replace method on "et" in the "afterTextChanged" method, the number isn't even formatting. With the code below the listener works and everything, i just don't know how to replace the "," with "." until the decimals.
If you think to just change the pattern ("###,##0,00") it doesn't work
This is the TextWatcher I have for the EditText
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;
public class NumberTextWatcher implements TextWatcher {
private DecimalFormat df;
private DecimalFormat dfnd;
private boolean hasFractionalPart;
private EditText et;
public NumberTextWatcher(EditText et)
{
df = new DecimalFormat("###,##0,00");
df.setDecimalSeparatorAlwaysShown(true);
dfnd = new DecimalFormat("###,##0,00");
this.et = et;
hasFractionalPart = false;
}
#SuppressWarnings("unused")
private static final String TAG = "NumberTextWatcher";
#Override
public void afterTextChanged(Editable s)
{
et.removeTextChangedListener(this);
try {
int inilen, endlen;
inilen = et.getText().length();
String v = s.toString().replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
Number n = df.parse(v);
int cp = et.getSelectionStart();
if (hasFractionalPart) {
et.setText(df.format(n));
} else {
et.setText(dfnd.format(n));
}
endlen = et.getText().length();
int sel = (cp + (endlen - inilen));
if (sel > 0 && sel <= et.getText().length()) {
et.setSelection(sel);
} else {
// place cursor at the end?
et.setSelection(et.getText().length() - 1);
}
} catch (NumberFormatException nfe) {
// do nothing?
} catch (ParseException e) {
// do nothing?
}
et.addTextChangedListener(this);
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after)
{
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
int index = s.toString().indexOf(String.valueOf(df.getDecimalFormatSymbols().getDecimalSeparator()));
int trailingZeroCount = 0;
if (index > -1)
{
for (index++; index < s.length(); index++) {
if (s.charAt(index) == '0')
trailingZeroCount++;
else {
trailingZeroCount = 0;
}
}
hasFractionalPart = true;
} else {
hasFractionalPart = false;
}
{
if (s.toString().contains(String.valueOf(df.getDecimalFormatSymbols().getDecimalSeparator())))
{
hasFractionalPart = true;
} else {
hasFractionalPart = false;
}
}
}
}

Use this:
DecimalFormat decimalFormat=new DecimalFormat();
DecimalFormatSymbols decimalFormatSymbols=DecimalFormatSymbols.getInstance();
decimalFormatSymbols.setDecimalSeparator(',');
decimalFormatSymbols.setGroupingSeparator('.');
decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);
String formattedNumber=decimalFormat.format(123456.78);// prints 123.456,78

Related

DecimalFormat issue with EditText Input

Hello i'm writing this program to get user input and format to 0,000.00 for example
the program works fine when i try 5,768.80 BUT it doesn't when i try 5,768.08
as you can see the problem is that i can't put a 0 in the cents place before any other number... here is my code:
package com.calculadorabss.gorydev.calculadorabolivaressoberanos;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
import java.text.DecimalFormat;
import java.text.ParseException;
class NumberTextWatcher implements TextWatcher {
private DecimalFormat df;
private DecimalFormat dfnd;
private boolean hasFractionalPart;
private EditText et;
//Dar formato al texto de entrada separando por comas
public NumberTextWatcher(EditText et)
{
df = new DecimalFormat("#,###,##");
df.setDecimalSeparatorAlwaysShown(true);
dfnd = new DecimalFormat("#,###,##");
this.et = et;
hasFractionalPart = false;
}
#SuppressWarnings("unused")
private static final String TAG = "NumberTextWatcher";
public void afterTextChanged(Editable s)
{
et.removeTextChangedListener(this);
try {
int inilen, endlen;
inilen = et.getText().length();
String v = s.toString().replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
Number n = df.parse(v);
int cp = et.getSelectionStart();
if (hasFractionalPart) {
et.setText(df.format(n));
} else {
et.setText(dfnd.format(n));
}
endlen = et.getText().length();
int sel = (cp + (endlen - inilen));
if (sel > 0 && sel <= et.getText().length()) {
et.setSelection(sel);
} else {
// place cursor at the end?
et.setSelection(et.getText().length() - 1);
}
} catch (NumberFormatException nfe) {
// do nothing?
} catch (ParseException e) {
// do nothing?
}
et.addTextChangedListener(this);
}
}
i want the program to be able to receive 0 as cents for example 67,789.05
Try this i think it work
public void afterTextChanged(Editable s)
{
et.removeTextChangedListener(this);
try {
int inilen, endlen;
inilen = et.getText().length();
String v = s.toString().replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
hasFractionalPart = v.contains(".");
Number n = df.parse(v);
int cp = et.getSelectionStart();
if (!hasFractionalPart) {
et.setText(dfnd.format(n));
}
endlen = et.getText().length();
int sel = (cp + (endlen - inilen));
if (sel > 0 && sel <= et.getText().length()) {
et.setSelection(sel);
} else {
// place cursor at the end?
et.setSelection(et.getText().length() - 1);
}
} catch (NumberFormatException nfe) {
// do nothing?
} catch (ParseException e) {
// do nothing?
}
et.addTextChangedListener(this);
}

DecimalFormat java allow to type 0 after dot

I have a DecimalFormat with the following pattern "#,###.###"
But when I want to type for example 50.05 it does not allow to, as I parse the string to number every time a text changes and when I call
df.parse("50.0");
it returns 50. and cuts the 0
Any ideas what can I do to be able to type numbers like 50.05 ?
The full code of my class is presented below. Sorry if it is too verbose:
private class NumberTextWatcher implements TextWatcher {
#SuppressWarnings("unused")
private static final String TAG = "NumberTextWatcher";
private DecimalFormat df;
private DecimalFormat dfnd;
private boolean hasFractionalPart;
private EditText et;
NumberTextWatcher(EditText et) {
df = (DecimalFormat) DecimalFormat.getInstance(Locale.ENGLISH);
df.applyLocalizedPattern(hasThousandSeparator ? "#,###.###" : "####.###");
df.setDecimalSeparatorAlwaysShown(true);
df.setMaximumFractionDigits(maxDecimal);
dfnd = (DecimalFormat) DecimalFormat.getInstance(Locale.ENGLISH);
dfnd.applyLocalizedPattern(hasThousandSeparator ? "#,###" : "####");
dfnd.setMaximumFractionDigits(maxDecimal);
this.et = et;
hasFractionalPart = false;
}
#Override
public void afterTextChanged(Editable s) {
et.removeTextChangedListener(this);
try {
int inilen, endlen;
inilen = et.getText().length();
String sign = hasSign ? s.toString().startsWith("-") ? "-" : "+" : null;
String sStr = hasSignAttached(s) ? s.toString().substring(1) : s.toString();
String v = sStr.replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
if(v.length() == 0){
return;
}
if(hasFractionalPart){
String[] vSplit = v.split("\\.");
String intStr = vSplit[0];
String decStr = vSplit.length > 1 ? vSplit[1] : "";
if(intStr.length() > maxNumbers)
intStr = intStr.substring(0, maxNumbers);
if(decStr.length() > maxDecimal)
decStr = decStr.substring(0, maxDecimal);
v = String.format("%s.%s", intStr, decStr);
} else {
if(v.length() > maxNumbers)
v = v.substring(0, maxNumbers);
}
Number n = df.parse(v);
int cp = et.getSelectionStart();
String formatted;
if (hasFractionalPart) {
formatted = df.format(n);
} else {
formatted = dfnd.format(n);
}
et.setText(hasSign ? String.format("%s%s", sign, formatted) : formatted);
endlen = et.getText().length();
int sel = (cp + (endlen - inilen));
if (sel > 0 && sel <= et.getText().length()) {
et.setSelection(sel);
} else {
// place cursor at the end?
et.setSelection(et.getText().length() - 1);
}
} catch (Exception e) {
ExceptionTracker.trackException(e);
} finally {
et.addTextChangedListener(this);
}
}
private boolean hasSignAttached(Editable s){
return s.toString().startsWith("+") || s.toString().startsWith("-");
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if(hasSign && start == 1 && count == 1){
et.setText("");
}
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
hasFractionalPart = s.toString().contains(String.valueOf(df.getDecimalFormatSymbols().getDecimalSeparator()));
}
}
this code solves the problem:
import java.text.DecimalFormat;
public class DecimalFormatExample {
public static void main(String[] args) {
DecimalFormat dformat = new DecimalFormat("#,###.###");
dformat.setMinimumFractionDigits(1);
String someNumber = "50.05";
Double someNumberDouble = Double.valueOf(someNumber);
System.out.println(dformat.format(someNumberDouble)); // 50.05
someNumber = "50.0";
someNumberDouble = Double.valueOf(someNumber);
System.out.println(dformat.format(someNumberDouble)); // 50.0
}
}
Ok, I have done a small hack to fix this. May be it will help someone in the future, or someone will suggest a better solution.
So I added the following checks:
boolean decHasZeroAtEnd = false;
.......
if(decStr.endsWith("0")){
decHasZeroAtEnd = true;
}
.......
if(decHasZeroAtEnd){
decHasZeroAtEnd = false;
formatted = String.format("%s0", formatted);
}
And the whole code is the following:
private class NumberTextWatcher implements TextWatcher {
#SuppressWarnings("unused")
private static final String TAG = "NumberTextWatcher";
private DecimalFormat df;
private DecimalFormat dfnd;
private boolean hasFractionalPart;
private EditText et;
NumberTextWatcher(EditText et) {
df = (DecimalFormat) DecimalFormat.getInstance(Locale.ENGLISH);
df.applyLocalizedPattern(hasThousandSeparator ? "#,###.###" : "####.###");
df.setDecimalSeparatorAlwaysShown(true);
df.setMaximumFractionDigits(maxDecimal);
dfnd = (DecimalFormat) DecimalFormat.getInstance(Locale.ENGLISH);
dfnd.applyLocalizedPattern(hasThousandSeparator ? "#,###" : "####");
dfnd.setMaximumFractionDigits(maxDecimal);
this.et = et;
hasFractionalPart = false;
}
#Override
public void afterTextChanged(Editable s) {
et.removeTextChangedListener(this);
try {
int inilen, endlen;
inilen = et.getText().length();
String sign = hasSign ? s.toString().startsWith("-") ? "-" : "+" : null;
String sStr = hasSignAttached(s) ? s.toString().substring(1) : s.toString();
String v = sStr.replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
if(v.length() == 0){
return;
}
boolean decHasZeroAtEnd = false;
if(hasFractionalPart){
String[] vSplit = v.split("\\.");
String intStr = vSplit[0];
String decStr = vSplit.length > 1 ? vSplit[1] : "";
if(intStr.length() > maxNumbers)
intStr = intStr.substring(0, maxNumbers);
if(decStr.length() > maxDecimal)
decStr = decStr.substring(0, maxDecimal);
if(decStr.endsWith("0")){
decHasZeroAtEnd = true;
}
v = String.format("%s.%s", intStr, decStr);
} else {
if(v.length() > maxNumbers)
v = v.substring(0, maxNumbers);
}
Number n = df.parse(v);
int cp = et.getSelectionStart();
String formatted;
if (hasFractionalPart) {
formatted = df.format(n);
} else {
formatted = dfnd.format(n);
}
if(decHasZeroAtEnd){
decHasZeroAtEnd = false;
formatted = String.format("%s0", formatted);
}
Log.d("testt", "formatted " + formatted);
et.setText(hasSign ? String.format("%s%s", sign, formatted) : formatted);
endlen = et.getText().length();
int sel = (cp + (endlen - inilen));
if (sel > 0 && sel <= et.getText().length()) {
et.setSelection(sel);
} else {
// place cursor at the end?
et.setSelection(et.getText().length() - 1);
}
} catch (Exception e) {
ExceptionTracker.trackException(e);
} finally {
et.addTextChangedListener(this);
}
}
private boolean hasSignAttached(Editable s){
return s.toString().startsWith("+") || s.toString().startsWith("-");
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if(hasSign && start == 1 && count == 1){
et.setText("");
}
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
hasFractionalPart = s.toString().contains(String.valueOf(df.getDecimalFormatSymbols().getDecimalSeparator()));
}
}

EditText caches old text

I have following issue. I have EditText and TextWatcher which format input text according to some rules. In method afterTextChanged() I format it. Then I have formatted string and I want to replace old value of EditText by formatted value.
Next we have two options:
use EditText.setText()
use Editable.replace()
If we use first option, EditText works very slowly and looses symbols.
But If we use second method, Editable doesn't replace old text, but append new text to old text.
Maybe someone had similar issue?
Upd: using Editable.clear() then Editable.append() or insert() have similar effect
Code:
public static class LoginWatcher implements TextWatcher {
private EditText target;
private LoginFilter loginFilter = new LoginFilter();
private int lastLength;
private boolean wasPhoneNumber = false;
private AsYouTypeFormatter formatter;
private boolean isFormattingStopped;
public LoginWatcher(OnLoginEnterListener onLoginInputListener, EditText target) {
listener = onLoginInputListener;
this.target = target;
lastLength = target.getText().length();
formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry());
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (isFormattingStopped) {
return;
}
if (count > 0 && hasSeparator(s, start, count)) {
stopFormatting();
}
}
#Override
public void afterTextChanged(Editable s) {
target.removeTextChangedListener(this);
boolean isSymbolsChecked = loginFilter.check(s.toString());
boolean isEmail = StringUtils.isEmailValid(s.toString());
boolean isPhoneNumber = isPhoneNumber(s.toString());
if (lastLength <= s.length()) {
if (isPhoneNumber && !isFormattingStopped) {
String formatted = reformat(s, Selection.getSelectionEnd(s));
if (formatted != null) {
target.setText(formatted);
target.setSelection(target.getText().length());
}
} else if (wasPhoneNumber) {
String unformatted = unFormatPhoneNumber(s.toString());
target.setText(unformatted); // or s.clear(); s.append();
target.setSelection(target.getText().length());
}
}
lastLength = s.length();
wasPhoneNumber = isPhoneNumber;
if (isFormattingStopped) {
isFormattingStopped = s.length() != 0;
}
target.addTextChangedListener(this);
}
private String unFormatPhoneNumber(String s) {
char[] chars = s.toCharArray();
if (s.isEmpty()) {
return s;
}
if (chars[0] == '+') {
boolean isPhoneNumber = true;
for (int i = 1; i < chars.length; ++i) {
if (!Character.isDigit(chars[i])) {
isPhoneNumber = false;
break;
}
}
if (isPhoneNumber) {
return s;
}
}
return s.replaceAll("[\\+\\(\\)\\s\\-]+", "");
}
private String reformat(CharSequence s, int cursor) {
int curIndex = cursor - 1;
String formatted = null;
formatter.clear();
char lastNonSeparator = 0;
boolean hasCursor = false;
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (PhoneNumberUtils.isNonSeparator(c)) {
if (lastNonSeparator != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor);
hasCursor = false;
}
lastNonSeparator = c;
}
if (i == curIndex) {
hasCursor = true;
}
}
if (lastNonSeparator != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor);
}
return formatted;
}
private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) {
return hasCursor ? formatter.inputDigitAndRememberPosition(lastNonSeparator)
: formatter.inputDigit(lastNonSeparator);
}
private boolean isPhoneNumber(String s) {
return !TextUtils.isEmpty(s) && Patterns.PHONE.matcher(s).matches();
}
private boolean hasSeparator(final CharSequence s, final int start, final int count) {
for (int i = start; i < start + count; i++) {
char c = s.charAt(i);
if (!PhoneNumberUtils.isNonSeparator(c)) {
return true;
}
}
return false;
}
private void stopFormatting() {
isFormattingStopped = true;
formatter.clear();
}
}
Try to use the methods provided by Editable
#Override
public void afterTextChanged(Editable s) {
target.removeTextChangedListener(this);
boolean isSymbolsChecked = loginFilter.check(s.toString());
boolean isEmail = StringUtils.isEmailValid(s.toString());
boolean isPhoneNumber = isPhoneNumber(s.toString());
if (lastLength <= s.length()) {
if (isPhoneNumber && !isFormattingStopped) {
String formatted = reformat(s, Selection.getSelectionEnd(s));
if (formatted != null) {
s.replace(0, s.length(), formatted)
target.setSelection(formatted.length());
}
} else if (wasPhoneNumber) {
String unformatted = unFormatPhoneNumber(s.toString());
s.replace(0, s.length(), formatted)
target.setSelection(formatted.length());
}
}
lastLength = s.length();
wasPhoneNumber = isPhoneNumber;
if (isFormattingStopped) {
isFormattingStopped = s.length() != 0;
}
target.addTextChangedListener(this);
}

EditText mask with 3 decimal places in java [android]

i'm newbie in Android and i want to do a mask for an EditText with a decimal number with 3 decimal places(ex: 0.658), i need a mask that user doesn't need write the ".", only the numbers, like a conventional mask for currency.
I'm trying create a TextWatcher based in this:
public static TextWatcher currency(final EditText editText) {
return new TextWatcher() {
String current = "";
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (!s.toString().equals(current)) {
editText.removeTextChangedListener(this);
String cleanString = s.toString();
if(count != 0) {
String substr = cleanString.substring(cleanString.length() - 2);
if (substr.contains(".") || substr.contains(",")) {
cleanString += "0";
}
}
cleanString = cleanString.replaceAll("[R$,.]", "");
double parsed = Double.parseDouble(cleanString);
Locale locale = new Locale("pt", "BR");
String formatted = NumberFormat.getCurrencyInstance(locale).format((parsed / 100));
formatted = formatted.replaceAll("[R$]", "");
current = formatted;
editText.setText(formatted);
editText.setSelection(formatted.length());
editText.addTextChangedListener(this);
}
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
public void afterTextChanged(Editable s) {}
};
}
But without success.
There are a better way to do this?
Thanks.
Try to use MaskedEditText from github.
I had a similar problem. So I started looking for any implementation of TextWatcher. I finally decided to build my own:
public class BrRealMoneyTextWatcher implements TextWatcher {
private static final Locale DEFAULT_LOCALE = new Locale("pt", "BR");
private static DecimalFormat NUMBER_FORMAT = (DecimalFormat) DecimalFormat.getCurrencyInstance(DEFAULT_LOCALE);
private static final int FRACTION_DIGITS = 2;
private static final String DECIMAL_SEPARATOR;
private static final String CURRENCY_SIMBOL;
static {
NUMBER_FORMAT.setMaximumFractionDigits(FRACTION_DIGITS);
NUMBER_FORMAT.setMaximumFractionDigits(FRACTION_DIGITS);
NUMBER_FORMAT.setParseBigDecimal(true);
DECIMAL_SEPARATOR = String.valueOf(NUMBER_FORMAT.getDecimalFormatSymbols().getDecimalSeparator());
CURRENCY_SIMBOL = NUMBER_FORMAT.getCurrency().getSymbol(DEFAULT_LOCALE);
}
final EditText target;
public BrRealMoneyTextWatcher(EditText target) {
this.target = target;
}
private boolean updating = false;
#Override
public void onTextChanged(CharSequence s, int start, int before, int after) {
if (updating) {
updating = false;
return;
}
updating = true;
try {
String valueStr = formatNumber(fixDecimal(s.toString()));
BigDecimal parsedValue = ((BigDecimal) NUMBER_FORMAT.parse(valueStr));
String value = NUMBER_FORMAT.format(parsedValue);
target.setText(value);
target.setSelection(value.length());
} catch (ParseException | NumberFormatException ex) {
throw new IllegalArgumentException("Erro ao aplicar a máscara", ex);
}
}
private String formatNumber(String originalNumber) {
String number = originalNumber.replaceAll("[^\\d]", "");
switch(number.length()) {
case 0 :
number = "0" + DECIMAL_SEPARATOR + "00";
break;
case 1 :
number = "0" + DECIMAL_SEPARATOR + "0" + number;
break;
case 2 :
number = "0" + DECIMAL_SEPARATOR + number;
break;
default:
number = number.substring(0, number.length() - 2) + DECIMAL_SEPARATOR + number.substring(number.length() - 2);
break;
}
return CURRENCY_SIMBOL + number;
}
private String fixDecimal(String number) {
int dotPos = number.indexOf('.') + 1;
int length = number.length();
return (length - dotPos < FRACTION_DIGITS) ? fixDecimal(number + "0") : number;
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void afterTextChanged(Editable s) {
}
}
I hope it helps somebody else.

Format currency string using EditText in Android

I'm trying to format the input value of an EditText in Android, I want to format the input in currency value, I' ve tried the following:
EditText minimo = (EditText) view.findViewById(R.id.minimo);
Locale locale = new Locale("en", "UK");
NumberFormat fmt = NumberFormat.getCurrencyInstance(locale);
Locale locale = new Locale("en", "UK");
NumberFormat fmt = NumberFormat.getCurrencyInstance(locale);
minimo.setText("", TextView.BufferType.valueOf(String.valueOf(fmt.format(TextView.BufferType.EDITABLE))));
Log:
java.lang.IllegalArgumentException: Bad class: class android.widget.TextView$BufferType
try this :
set TextChangedListner as :
minimo.addTextChangedListener(new NumberTextWatcher(minimo));
create custom TextWatcher as:
class NumberTextWatcher implements TextWatcher {
private DecimalFormat df;
private DecimalFormat dfnd;
private boolean hasFractionalPart;
private EditText et;
public NumberTextWatcher(EditText et)
{
df = new DecimalFormat("#,###.##");
df.setDecimalSeparatorAlwaysShown(true);
dfnd = new DecimalFormat("#,###");
this.et = et;
hasFractionalPart = false;
}
#SuppressWarnings("unused")
private static final String TAG = "NumberTextWatcher";
public void afterTextChanged(Editable s)
{
et.removeTextChangedListener(this);
try {
int inilen, endlen;
inilen = et.getText().length();
String v = s.toString().replace(String.valueOf(df.getDecimalFormatSymbols().getGroupingSeparator()), "");
Number n = df.parse(v);
int cp = et.getSelectionStart();
if (hasFractionalPart) {
et.setText(df.format(n));
} else {
et.setText(dfnd.format(n));
}
endlen = et.getText().length();
int sel = (cp + (endlen - inilen));
if (sel > 0 && sel <= et.getText().length()) {
et.setSelection(sel);
} else {
// place cursor at the end?
et.setSelection(et.getText().length() - 1);
}
} catch (NumberFormatException nfe) {
// do nothing?
} catch (ParseException e) {
// do nothing?
}
et.addTextChangedListener(this);
}
public void beforeTextChanged(CharSequence s, int start, int count, int after)
{
}
public void onTextChanged(CharSequence s, int start, int before, int count)
{
if (s.toString().contains(String.valueOf(df.getDecimalFormatSymbols().getDecimalSeparator())))
{
hasFractionalPart = true;
} else {
hasFractionalPart = false;
}
}
}

Categories

Resources