56

How can I format an EditText to follow the "dd/mm/yyyy" format the same way that we can format using a TextWatcher to mask the user input to look like "0.05€". I'm not talking about limiting the characters, or validating a date, just masking to the previous format.

Juan Cortés
  • 18,689
  • 8
  • 63
  • 88

10 Answers10

110

I wrote this TextWatcher for a project, hopefully it will be helpful to someone. Note that it does not validate the date entered by the user, and you should handle that when the focus changes, since the user may not have finished entering the date.

Update 25/06 Made it a wiki to see if we reach a better final code.

Update 07/06 I finally added some sort of validation to the watcher itself. It will do the following with invalid dates:

  • If the month is greater than 12, it will be 12 (December)
  • If the date is greater than the one for the month selected, make it the max for that month.
  • If the year is not in the range 1900-2100, change it to be in the range

This validation fits my needs, but some of you may want to change it a little bit, ranges are easily changeable and you could hook this validations to Toast message for instance, to notify the user that we've modified his/her date since it was invalid.

In this code, I will be assuming that we have a reference to our EditText called date that has this TextWatcher attached to it, this can be done something like this:

EditText date;
date = (EditText)findViewById(R.id.whichdate);
date.addTextChangedListener(tw);

TextWatcher tw = new TextWatcher() {
    private String current = "";
    private String ddmmyyyy = "DDMMYYYY";
    private Calendar cal = Calendar.getInstance();

When user changes text of the EditText

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        if (!s.toString().equals(current)) {
            String clean = s.toString().replaceAll("[^\\d.]|\\.", "");
            String cleanC = current.replaceAll("[^\\d.]|\\.", "");

            int cl = clean.length();
            int sel = cl;
            for (int i = 2; i <= cl && i < 6; i += 2) {
                sel++;
            }
            //Fix for pressing delete next to a forward slash
            if (clean.equals(cleanC)) sel--;

            if (clean.length() < 8){
               clean = clean + ddmmyyyy.substring(clean.length());
            }else{
               //This part makes sure that when we finish entering numbers
               //the date is correct, fixing it otherwise
               int day  = Integer.parseInt(clean.substring(0,2));
               int mon  = Integer.parseInt(clean.substring(2,4));
               int year = Integer.parseInt(clean.substring(4,8));

               mon = mon < 1 ? 1 : mon > 12 ? 12 : mon;
               cal.set(Calendar.MONTH, mon-1);
               year = (year<1900)?1900:(year>2100)?2100:year;
               cal.set(Calendar.YEAR, year); 
               // ^ first set year for the line below to work correctly
               //with leap years - otherwise, date e.g. 29/02/2012
               //would be automatically corrected to 28/02/2012 

               day = (day > cal.getActualMaximum(Calendar.DATE))? cal.getActualMaximum(Calendar.DATE):day;
               clean = String.format("%02d%02d%02d",day, mon, year);
            }

            clean = String.format("%s/%s/%s", clean.substring(0, 2),
                clean.substring(2, 4),
                clean.substring(4, 8));

            sel = sel < 0 ? 0 : sel;
            current = clean;
            date.setText(current);
            date.setSelection(sel < current.length() ? sel : current.length());
        }
    }

We also implement the other two functions because we have to

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

    @Override
    public void afterTextChanged(Editable s) {}
};

This produces the following effect, where deleting or inserting characters will reveal or hide the dd/mm/yyyy mask. It should be easy to modify to fit other format masks since I tried to leave the code as simple as possible.

enter image description here

David Rearte
  • 439
  • 4
  • 17
Juan Cortés
  • 18,689
  • 8
  • 63
  • 88
  • 1
    I'm sorry. How can I access the full code? What is your cal variable? – joao2fast4u Apr 04 '14 at 11:13
  • 3
    @joao2fast4u - cal variable is java.util.Calendar object – Leonardo Otto Sep 30 '14 at 17:43
  • 1
    @joao2fast4u @LeonardoOtto For newbies like me: I was getting a hard time until I had `cal` declared like this: `private Calendar cal = Calendar.getInstance();` or else I'd get a `NullPointerException` at the `cal.set(Calendar.MONTH, mon-1);` part – Thomas Jan 22 '15 at 19:13
  • 1
    Sorry for the code not being newby proof, if anyone has the time to edit it, please feel free. That's why I made it a wiki – Juan Cortés Jan 22 '15 at 19:15
  • @Juan-devtopia.coop You're right. I made the change. Thanks for code, it helped a lot. – Thomas Jan 22 '15 at 19:43
  • But how can you set the EditText to display the structure dd/dd/dddd - where d is digit and '/' is a separator that remain on screen and cannot be edited? or maybe this is a different question? – GyRo Feb 03 '15 at 20:00
  • That's another question to me @GuyRoth :/ – Juan Cortés Feb 03 '15 at 20:27
  • The code works but sometimes it create a string like this 12/12/1999Y. I don't understand why it add the Y to the end of the final string. – ad3luc Feb 17 '16 at 09:42
  • Can you help me on the case when we have typed the whole date say 24/12/2015 and delete 4, then the whole text after the 4 shifts to the left. Is there any way to avoid that ? – DAS Dec 17 '16 at 12:08
  • There's always a way. Find out what has *changed* since the previous version of the value, and if it we've changed something other than the end, fill the gap with say... zeros. How to do that would be a different story, perhaps consider looking at the cursor position... Or an uglier ux would be to always have the cursor at the end.. – Juan Cortés Dec 21 '16 at 20:57
  • In my case (date format dd.mm.yyyy instead of dd/mm/yyyy) it worked only after this edit: String clean = s.toString().replaceAll("\\D", ""); String cleanC = current.replaceAll("\\D", ""); The initial version produced redundant dots each time user typed a character, just because former regular expression didn't eliminate them. I think it mistooks dots for decimal points which are the part of numbers. – 12oz Mouse Jun 06 '17 at 04:35
  • what is the current!? is it the previous content of EditText or current one? There are some unclear and confusing variables in the code i.e: `current`, `ddmmyyyy` ,etc. you have made it wiki, please clarify your code – null Oct 31 '17 at 02:07
  • There is an open bug in my system against your code. It is not possible to delete numbers from the center of the date... the whole string shifts left. For example, if, given the string `23/11/2016` I attempt to delete only `11` from the string, then I get the partial date `23/20/16YY`. – QED Feb 21 '18 at 22:17
  • Add validation for 00/01/1900 (i.e. cal.getActualMinimum()) **replace this :** `day = (day > cal.getActualMaximum(Calendar.DATE))? cal.getActualMaximum(Calendar.DATE):day;` **with following :** `int MIN_DATE = cal.getActualMinimum(Calendar.DATE); int MAX_DATE = cal.getActualMaximum(Calendar.DATE); day = (day < MIN_DATE) ? MIN_DATE : (day > MAX_DATE) ? MAX_DATE : day;` – Sha2nk Dec 26 '19 at 14:03
  • I change date format to YYYYMMDD. I manage to work but cannot delete the year part from the edittext – D.madushanka Dec 10 '20 at 06:46
17

The current answer is very good and helped guide me towards my own solution. There are a few reasons why I decided to post my own solution even though this question already has a valid answer:

  • I´m working in Kotlin, not Java. People who find themselves with the same issue will have to translate the current solution.
  • I wanted to write an answer that was more legible so that people can more easily adapt it to their own problems.
  • As suggested by dengue8830, I encapsulated the solution to this problem in a class, so anyone can use without even worrying about the implementation.

To use it, just do something like:

  • DateInputMask(mEditText).listen()

And the solution is shown below:

class DateInputMask(val input : EditText) {

    fun listen() {
        input.addTextChangedListener(mDateEntryWatcher)
    }

    private val mDateEntryWatcher = object : TextWatcher {

        var edited = false
        val dividerCharacter = "/"

        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
            if (edited) {
                edited = false
                return
            }

            var working = getEditText()

            working = manageDateDivider(working, 2, start, before)
            working = manageDateDivider(working, 5, start, before)

            edited = true
            input.setText(working)
            input.setSelection(input.text.length)
        }

        private fun manageDateDivider(working: String, position : Int, start: Int, before: Int) : String{
            if (working.length == position) {
                return if (before <= position && start < position)
                    working + dividerCharacter
                else
                    working.dropLast(1)
            }
            return working
        }

        private fun getEditText() : String {
            return if (input.text.length >= 10)
                input.text.toString().substring(0,10)
            else
                input.text.toString()
        }

        override fun afterTextChanged(s: Editable) {}
        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
    }
}
Ícaro Mota
  • 658
  • 7
  • 13
11

a cleaner way to use the Juan Cortés's code is put it in a class:

public class DateInputMask implements TextWatcher {

private String current = "";
private String ddmmyyyy = "DDMMYYYY";
private Calendar cal = Calendar.getInstance();
private EditText input;

public DateInputMask(EditText input) {
    this.input = input;
    this.input.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) {
    if (s.toString().equals(current)) {
        return;
    }

    String clean = s.toString().replaceAll("[^\\d.]|\\.", "");
    String cleanC = current.replaceAll("[^\\d.]|\\.", "");

    int cl = clean.length();
    int sel = cl;
    for (int i = 2; i <= cl && i < 6; i += 2) {
        sel++;
    }
    //Fix for pressing delete next to a forward slash
    if (clean.equals(cleanC)) sel--;

    if (clean.length() < 8){
        clean = clean + ddmmyyyy.substring(clean.length());
    }else{
        //This part makes sure that when we finish entering numbers
        //the date is correct, fixing it otherwise
        int day  = Integer.parseInt(clean.substring(0,2));
        int mon  = Integer.parseInt(clean.substring(2,4));
        int year = Integer.parseInt(clean.substring(4,8));

        mon = mon < 1 ? 1 : mon > 12 ? 12 : mon;
        cal.set(Calendar.MONTH, mon-1);
        year = (year<1900)?1900:(year>2100)?2100:year;
        cal.set(Calendar.YEAR, year);
        // ^ first set year for the line below to work correctly
        //with leap years - otherwise, date e.g. 29/02/2012
        //would be automatically corrected to 28/02/2012

        day = (day > cal.getActualMaximum(Calendar.DATE))? cal.getActualMaximum(Calendar.DATE):day;
        clean = String.format("%02d%02d%02d",day, mon, year);
    }

    clean = String.format("%s/%s/%s", clean.substring(0, 2),
            clean.substring(2, 4),
            clean.substring(4, 8));

    sel = sel < 0 ? 0 : sel;
    current = clean;
    input.setText(current);
    input.setSelection(sel < current.length() ? sel : current.length());
}

@Override
public void afterTextChanged(Editable s) {

}
}

then you can reuse it

new DateInputMask(myEditTextInstance);
David Rearte
  • 439
  • 4
  • 17
4

Try using a library that solves this problem since masking it's not available out of the box. There are a lot of corner cases (like adding/deleting characters in the middle of already masked text) and to properly handle this you'll end up with a lot of code (and bugs).

Here are some available libraries:
https://github.com/egslava/edittext-mask
https://github.com/dimitar-zabaznoski/MaskedEditText
https://github.com/pinball83/Masked-Edittext
https://github.com/RedMadRobot/input-mask-android
https://github.com/santalu/mask-edittext

** Mind that at the time of writing these libraries are not without issues, so it's your responsibility to choose which one fits you best and test the code.

kalimero
  • 61
  • 2
3

Kotlin version without validation

        editText.addTextChangedListener(object : TextWatcher{

            var sb : StringBuilder = StringBuilder("")

            var _ignore = false

            override fun afterTextChanged(s: Editable?) {}

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

            if(_ignore){
                _ignore = false
                return
            }

            sb.clear()
            sb.append(if(s!!.length > 10){ s.subSequence(0,10) }else{ s })

            if(sb.lastIndex == 2){
                if(sb[2] != '/'){
                    sb.insert(2,"/")
                }
            } else if(sb.lastIndex == 5){
                if(sb[5] != '/'){
                    sb.insert(5,"/")
                }
            }

            _ignore = true
            editText.setText(sb.toString())
            editText.setSelection(sb.length)

        }
    })
Saurabh Padwekar
  • 2,982
  • 1
  • 24
  • 29
2

Juan Cortés' wiki works like a charm https://stackoverflow.com/a/16889503/3480740

Here my Kotlin version

fun setBirthdayEditText() {

    birthdayEditText.addTextChangedListener(object : TextWatcher {

        private var current = ""
        private val ddmmyyyy = "DDMMYYYY"
        private val cal = Calendar.getInstance()

        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            if (p0.toString() != current) {
                var clean = p0.toString().replace("[^\\d.]|\\.".toRegex(), "")
                val cleanC = current.replace("[^\\d.]|\\.", "")

                val cl = clean.length
                var sel = cl
                var i = 2
                while (i <= cl && i < 6) {
                    sel++
                    i += 2
                }
                //Fix for pressing delete next to a forward slash
                if (clean == cleanC) sel--

                if (clean.length < 8) {
                    clean = clean + ddmmyyyy.substring(clean.length)
                } else {
                    //This part makes sure that when we finish entering numbers
                    //the date is correct, fixing it otherwise
                    var day = Integer.parseInt(clean.substring(0, 2))
                    var mon = Integer.parseInt(clean.substring(2, 4))
                    var year = Integer.parseInt(clean.substring(4, 8))

                    mon = if (mon < 1) 1 else if (mon > 12) 12 else mon
                    cal.set(Calendar.MONTH, mon - 1)
                    year = if (year < 1900) 1900 else if (year > 2100) 2100 else year
                    cal.set(Calendar.YEAR, year)
                    // ^ first set year for the line below to work correctly
                    //with leap years - otherwise, date e.g. 29/02/2012
                    //would be automatically corrected to 28/02/2012

                    day = if (day > cal.getActualMaximum(Calendar.DATE)) cal.getActualMaximum(Calendar.DATE) else day
                    clean = String.format("%02d%02d%02d", day, mon, year)
                }

                clean = String.format("%s/%s/%s", clean.substring(0, 2),
                        clean.substring(2, 4),
                        clean.substring(4, 8))

                sel = if (sel < 0) 0 else sel
                current = clean
                birthdayEditText.setText(current)
                birthdayEditText.setSelection(if (sel < current.count()) sel else current.count())
            }
        }

        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        }

        override fun afterTextChanged(p0: Editable) {

        }
    })
}
ivoroto
  • 597
  • 6
  • 9
  • It works, it would be good to define the method as an extension function on the Edittext class – Nicola Gallazzi Sep 19 '19 at 14:08
  • Also you missed one thing `val cleanC = current.replace("[^\\d.]|\\.", "")` should be `val cleanC = current.replace("[^\\d.]|\\.".toRegex(), "")` – Estevex Aug 12 '20 at 11:38
2

add android:inputType="date" to your EditText

Alireza Jamali
  • 603
  • 2
  • 4
  • 16
1

This answer does not apply a full mask for the remaining untyped digits. However, it is related and is the solution I needed. It works similar to how PhoneNumberFormattingTextWatcher works.

As you type it adds slashes to separate a date formatted like mm/dd/yyyy. It does not do any validation - just formatting.

No need for an EditText reference. Just set the listener and it works. myEditText.addTextChangedListener(new DateTextWatcher());

import android.text.Editable;
import android.text.TextWatcher;

import java.util.Locale;

/**
 * Adds slashes to a date so that it matches mm/dd/yyyy.
 *
 * Created by Mark Miller on 12/4/17.
 */
public class DateTextWatcher implements TextWatcher {

    public static final int MAX_FORMAT_LENGTH = 8;
    public static final int MIN_FORMAT_LENGTH = 3;

    private String updatedText;
    private boolean editing;


    @Override
    public void beforeTextChanged(CharSequence charSequence, int start, int before, int count) {

    }

    @Override
    public void onTextChanged(CharSequence text, int start, int before, int count) {
        if (text.toString().equals(updatedText) || editing) return;

        String digitsOnly = text.toString().replaceAll("\\D", "");
        int digitLen = digitsOnly.length();

        if (digitLen < MIN_FORMAT_LENGTH || digitLen > MAX_FORMAT_LENGTH) {
            updatedText = digitsOnly;
            return;
        }

        if (digitLen <= 4) {
            String month = digitsOnly.substring(0, 2);
            String day = digitsOnly.substring(2);

            updatedText = String.format(Locale.US, "%s/%s", month, day);
        }
        else {
            String month = digitsOnly.substring(0, 2);
            String day = digitsOnly.substring(2, 4);
            String year = digitsOnly.substring(4);

            updatedText = String.format(Locale.US, "%s/%s/%s", month, day, year);
        }
    }

    @Override
    public void afterTextChanged(Editable editable) {

        if (editing) return;

        editing = true;

        editable.clear();
        editable.insert(0, updatedText);

        editing = false;
    }
}
Markymark
  • 1,724
  • 22
  • 29
0

You can use below code and it adds all the validations for a date to be valid as well. Like days cnt be more than 31; month cant be greater than 12 etc.

class DateMask : TextWatcher {

private var updatedText: String? = null
private var editing: Boolean = false

companion object {

    private const val MAX_LENGTH = 8
    private const val MIN_LENGTH = 2
}


override fun beforeTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {

}

override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
    if (text.toString() == updatedText || editing) return

    var digits = text.toString().replace("\\D".toRegex(), "")
    val length = digits.length

    if (length <= MIN_LENGTH) {
        digits = validateMonth(digits)
        updatedText = digits
        return
    }

    if (length > MAX_LENGTH) {
        digits = digits.substring(0, MAX_LENGTH)
    }

    updatedText = if (length <= 4) {

        digits = validateDay(digits.substring(0, 2), digits.substring(2))
        val month = digits.substring(0, 2)
        val day = digits.substring(2)

        String.format(Locale.US, "%s/%s", month, day)
    } else {
        digits = digits.substring(0, 2) + digits.substring(2, 4) + validateYear(digits.substring(4))
        val month = digits.substring(0, 2)
        val day = digits.substring(2, 4)
        val year = digits.substring(4)

        String.format(Locale.US, "%s/%s/%s", month, day, year)
    }
}

private fun validateDay(month: String, day: String): String {

    val arr31 = intArrayOf(1, 3, 5, 7, 8, 10, 12)
    val arr30 = intArrayOf(4, 6, 9, 11)
    val arrFeb = intArrayOf(2)

    if (day.length == 1 &&
            ((day.toInt() > 3 && month.toInt() !in arrFeb)
                    || (day.toInt() > 2 && month.toInt() in arrFeb))) {
        return month
    }

    return when (month.toInt()) {
        in arr31 -> validateDay(month, arr31, day, 31)
        in arr30 -> validateDay(month, arr30, day, 30)
        in arrFeb -> validateDay(month, arrFeb, day, 29)
        else -> "$month$day"
    }

}

private fun validateDay(month: String, arr: IntArray, day: String, maxDay: Int): String {
    if (month.toInt() in arr) {
        if (day.toInt() > maxDay) {
            return "$month${day.substring(0, 1)}"
        }
    }
    return "$month$day"
}

private fun validateYear(year: String): String {
    if (year.length == 1 && (year.toInt() in 3..9 || year.toInt() == 0)) {
        return ""
    }

    if (year.length == 2 && year.toInt() !in 19..20) {
        return year.substring(0, 1)
    }

    return year
}

private fun validateMonth(month: String): String {

    if (month.length == 1 && month.toInt() in 2..9) {
        return "0$month"
    }

    if (month.length == 2 && month.toInt() > 12) {
        return month.substring(0, 1)
    }
    return month
}

override fun afterTextChanged(editable: Editable) {

    if (editing) return

    editing = true

    editable.clear()
    editable.insert(0, updatedText)

    editing = false
}

}

In your fragment or Activity you can use this DateMask as this : mEditText?.addTextChangedListener(dateMask)

Ranjeet
  • 372
  • 2
  • 10
0

Spent 6 hours making my own format. Just give it a try and if you like it then go through the code. You can edit the date at any place like day only, month only. It will automatically escape the '/' character and update the next digit.

// initialized with the current date
String date = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(new Date());
edit_date_editEntity.setText(date);

public void formatDate(){
    edit_date_editEntity.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            String ss = s.toString();
            if (after==0) {         // when a single character is deleted
                if (s.charAt(start) == '/') {       // if the character is '/' , restore it and put the cursor at correct position
                    edit_date_editEntity.setText(s);
                    edit_date_editEntity.setSelection(start);
                }
                else if (s.charAt(start) == '-') {  // if the character is '-' , restore it and put the cursor at correct position
                    edit_date_editEntity.setText(s);
                    edit_date_editEntity.setSelection(start);
                }
                else if (ss.charAt(start) >= '0' && ss.charAt(start) <= '9') {  // if the character is a digit, replace it with '-'
                    ss = ss.substring(0, start) + "-" + ss.substring(start +1, ss.length());
                    edit_date_editEntity.setText(ss);
                    edit_date_editEntity.setSelection(start);
                }
            }
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            String ss = s.toString();
            if (before==0 ){    // when a single character is added
                if (edit_date_editEntity.getSelectionStart()==3 || edit_date_editEntity.getSelectionStart()==6) {
                    // if the new character was just before '/' character
                    // getSelection value gets incremented by 1, because text has been changed and hence cursor position updated
                    // Log.d("test", ss);
                    ss = ss.substring(0, start) + "/" + ss.substring(start, start + 1) + ss.substring(start + 3, ss.length());
                    // Log.d("test", ss);
                    edit_date_editEntity.setText(ss);
                    edit_date_editEntity.setSelection(start + 2);
                }
                else {
                    if (edit_date_editEntity.getSelectionStart()==11){
                        // if cursor was at last, do not add anything
                        ss = ss.substring(0,ss.length()-1);
                        edit_date_editEntity.setText(ss);
                        edit_date_editEntity.setSelection(10);
                    }
                    else {
                        // else replace the next digit with the entered digit
                        ss = ss.substring(0, start + 1) + ss.substring(start + 2, ss.length());
                        edit_date_editEntity.setText(ss);
                        edit_date_editEntity.setSelection(start + 1);
                    }
                }
            }
        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });
}
marc_s
  • 675,133
  • 158
  • 1,253
  • 1,388