I created method for parsing a view different date formats during data import (400 K records). My method catches ParseException and trying to parse date with next format when it's different.
Question: Is better way(and faster) to set correct date format during data import?
private static final String DMY_DASH_FORMAT = "dd-MM-yyyy";
private static final String DMY_DOT_FORMAT = "dd.MM.yyyy";
private static final String YMD_DASH_FORMAT = "yyyy-MM-dd";
private static final String YMD_DOT_FORMAT = "yyyy.MM.dd";
private static final String SIMPLE_YEAR_FORMAT = "yyyy";
private final List<String> dateFormats = Arrays.asList(YMD_DASH_FORMAT, DMY_DASH_FORMAT,
DMY_DOT_FORMAT, YMD_DOT_FORMAT);
private Date parseDateFromString(String date) throws ParseException {
if (date.equals("0")) {
return null;
}
if (date.length() == 4) {
SimpleDateFormat simpleDF = new SimpleDateFormat(SIMPLE_YEAR_FORMAT);
simpleDF.setLenient(false);
return new Date(simpleDF.parse(date).getTime());
}
for (String format : dateFormats) {
SimpleDateFormat simpleDF = new SimpleDateFormat(format);
try {
return new Date(simpleDF.parse(date).getTime());
} catch (ParseException exception) {
}
}
throw new ParseException("Unknown date format", 0);
}
If you're running single threaded, an obvious improvement is to create the SimpleDateFormat objects only once. In a multithreaded situation using ThreadLocal<SimpleDateFormat> would be required.
Also fix your exception handling. It looks like it's written by someone who shouldn't be trusted to import any data.
For a similar problem statememt , i had used time4j library in the past. Here is an example. This uses the following dependencies given below as well
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.time4j.PlainDate;
import net.time4j.format.expert.ChronoFormatter;
import net.time4j.format.expert.MultiFormatParser;
import net.time4j.format.expert.ParseLog;
import net.time4j.format.expert.PatternType;
public class MultiDateParser {
static final MultiFormatParser<PlainDate> MULTI_FORMAT_PARSER;
static {
ChronoFormatter<PlainDate> style1 = ChronoFormatter.ofDatePattern("dd-MM-yyyy", PatternType.CLDR,
Locale.GERMAN);
ChronoFormatter<PlainDate> style2 = ChronoFormatter.ofDatePattern("dd.MM.yyyy", PatternType.CLDR, Locale.US);
ChronoFormatter<PlainDate> style3 = ChronoFormatter.ofDatePattern("yyyy-MM-dd", PatternType.CLDR, Locale.US);
ChronoFormatter<PlainDate> style4 = ChronoFormatter.ofDatePattern("yyyy.MM.dd", PatternType.CLDR, Locale.US);
//this is not supported
//ChronoFormatter<PlainDate> style5 = ChronoFormatter.ofDatePattern("yyyy", PatternType.CLDR, Locale.US);
MULTI_FORMAT_PARSER = MultiFormatParser.of(style1, style2, style3, style4);
}
public List<PlainDate> parse() throws ParseException {
String[] input = { "11-09-2001", "09.11.2001", "2011-11-01", "2011.11.01", "2012" };
List<PlainDate> dates = new ArrayList<>();
ParseLog plog = new ParseLog();
for (String s : input) {
plog.reset(); // initialization
PlainDate date = MULTI_FORMAT_PARSER.parse(s, plog);
if (date == null || plog.isError()) {
System.out.println("Wrong entry found: " + s + " at position " + dates.size() + ", error-message="
+ plog.getErrorMessage());
} else {
dates.add(date);
}
}
System.out.println(dates);
return dates;
}
public static void main(String[] args) throws ParseException {
MultiDateParser mdp = new MultiDateParser();
mdp.parse();
}
}
<dependency>
<groupId>net.time4j</groupId>
<artifactId>time4j-core</artifactId>
<version>4.19</version>
</dependency>
<dependency>
<groupId>net.time4j</groupId>
<artifactId>time4j-misc</artifactId>
<version>4.19</version>
</dependency>
The case yyyy will have to be handled differently as it is not a date. May be similar logic that you have used (length ==4) is a choice.
The above code returns , you can check a quick perf run to see if this scales for the 400k records you have.
Wrong entry found: 2012 at position 4, error-message=Not matched by any format: 2012
[2001-09-11, 2001-11-09, 2011-11-01, 2011-11-01]
Talking about 400K records, it might be reasonable to do some "bare hands" optimization here.
For example: if your incoming string has a "-" on position 5, then you know that the only (potentially) matching format would be "yyyy-MM-dd". If it is "."; you know that it is the other format that starts yyyy.
So, if you really want to optimize, you could fetch that character and see what it is. Could save 3 attempts of parsing with the wrong format!
Beyond that: I am not sure if sure if "dd" means that your other dates start with "01" ... or if "1.1.2016" would be possible, too. If all your dates always use two digits for dd/mm; then you can repeat that game - as you would fetch on position 3 - to choose between "dd...." and "dd-....".
Of course; there is one disadvantage - if you follow that idea, you are very much "hard-coding" the expected formats into your code; so adding other formats will become harder. On the other hand; you would save a lot.
Finally: the other thing that might greatly speed up things would be to use stream operations for reading/parsing that information; because then you could look into parallel streams, and simply exploit the ability of modern hardware to process 4, 8, 16 dates in parallel.
Related
I have a list of files(approximately 500 or more files) where the filename contains a date.
file_20180810
file_19950101
file_20180809
etc.
What I want to do is delete files which exceed the storage period.
I've come up with the following logic so far
~Get dates of valid storage period (ie. if storage period is 5 days and date today is 20180810, store date values 20180810, 20180809, 20180808, 20180807, 20180806, 20180805 in an array.
~Check every file in a directory if it contains any of the following dates. If it contains date, don't delete, else delete.
My problem here is, if the file name does contain one single date and I use a loop to delete a file, it might delete other files with valid dates as well. To show what I want to do in code form, it goes somehow like this:
if (!fileName.contains(stringDate1) &&
!fileName.contains(stringDate2) &&
!fileName.contains(stringDate3)) //...until storage period
{//delete file}
Is there a better way to express this? Any suggestions for a workaround?
Please and thank you.
Parse dates from your filename. Here's an example:
import java.time.*;
import java.util.regex.*;
public class MyClass {
public static void main(String args[]) {
LocalDate today = LocalDate.now();
long storagePeriod = 5L;
String fileName = "file_20180804";
int year = 0;
int month = 0;
int day = 0;
String pattern = "file_(\\d{4})(\\d{2})(\\d{2})";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(fileName);
if (m.find()) {
year = Integer.parseInt(m.group(1));
month = Integer.parseInt(m.group(2));
day = Integer.parseInt(m.group(3));
}
LocalDate fileDate = LocalDate.of(year, month, day);
if (fileDate.isBefore(today.minusDays(storagePeriod))) {
System.out.println("Delete this file");
}
}
}
You can try using Regex to extract the actual date of each file and check for the inclusion in a validity period.
Pattern p = Pattern.compile("file_(?<date>\d{6})");
foreach(File f : filelist){
Matcher m = p.matcher(f.filename());
if(m.find()){
Date fileDate = new Date(m.group("date"));
if(fileDate.before(periodStartDate)){
file.delete();
}
}
}
The code is not precise and should not compile, check about Date object creation and comparison, but the main idea is pretty much here.
You can only delete Files that are not in the Array like (tested, working):
String path = ""; // <- Folder we want to clean.
DateFormat df = new SimpleDateFormat("yyyyMMdd"); // <- DateFormat to convert the Calendar dates into our format.
Calendar cal = Calendar.getInstance(); // <- Using Calendar to get the days backwards.
ArrayList<String> dr = new ArrayList<String>(); // <- Save the dates we want to remove. dr = don't remove
dr.add(df.format(cal.getTime())); // <- add the actual date to List
for(int i = 0; i < 5; i++) { // <- Loop 5 Times to get the 5 Last Days
cal.add(Calendar.DATE, -1); // <- remove 1 day from actual Calendar date
dr.add(df.format(cal.getTime())); // <- add the day before to List
}
for(File file : new File(path).listFiles()) { // <- loop through all the files in the folder
String filename = file.getName().substring(0, file.getName().lastIndexOf(".")); // <- name of the file without extension
boolean remove = true; // <- Set removing to "yes"
for(String s : dr) { // <- loop through all the allowed dates
if(filename.contains(s)) { // <- when the file contains the allowed date
remove = false; // <- Set removing to "no"
break; // <- Break the loop for better performance
}
}
if(remove) { // <- If remove is "yes"
file.delete(); // <- Delete the file because it's too old for us!
}
}
but this is not the best way! A much better method would be to calculate how old the files are. Because of the _ you can pretty easily get the dates from the filenames. Like (not tested):
String path = ""; // <- Folder we want to clean.
Date today = new Date();
DateFormat df = new SimpleDateFormat("yyyyMMdd"); // <- Dateformat you used in the files
long maxage = 5 * 24 * 60 * 60 * 1000; // <- Calculate how many milliseconds ago we want to delete
for(File file : new File(path).listFiles()) { // <- loop through all the files in the folder
String fds = file.getName().split("_")[1]; // <- Date from the filename as string
try {
Date date = df.parse(fds); // Convert the string to a date
if(date.getTime() - today.getTime() <= maxage) { // <- when the file is older as 5 days
file.delete(); // <- Delete the file
}
} catch (ParseException e) {
e.printStackTrace();
}
}
Here is some example code which demonstrates how a list of input files (file name strings, e.g., "file_20180810") can be verified against a supplied set of date strings (e.g., "20180810") and perform an operation (like delete the file) on them.
import java.util.*;
import java.io.*;
public class FilesTesting {
private static final int DATE_STRING_LENGTH = 8; // length of 20180809
public static void main(String [] args) {
List<String> filter = Arrays.asList("20180810", "20180808", "20180809", "20180807", "20180806", "20180805");
List<File> files = Arrays.asList(new File("file_20180810"), new File("file_19950101"), new File("file_20180809"));
for (File file : files) {
String fileDateStr = getDateStringFromFileName(file.getName());
if (filter.contains(fileDateStr)) {
// Do something with it
// Delete file - if it exists
System.out.println(file.toString());
}
}
}
private static String getDateStringFromFileName(String fileName) {
int fileLen = fileName.length();
int dateStrPos = fileLen - DATE_STRING_LENGTH;
return fileName.substring(dateStrPos);
}
}
If you’re using ES6 you can use array includes and return a true or false to validate.
['a', 'b', 'c'].includes('b')
I am trying to understand the need of using ThreadLocal. Lot of people mention ThreadLocal should be used to provide a Per-Thread SimpleDateFormat, but they do not mention how will a mangled SimpleDateFormat will be seen if ThreadLocal is not used. I try the following code, seems it is just fine, I don't see a mangled SimpleDateFormat.
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadLocalTest {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
private static final Date TODAY = new Date();
private static final String expected = "07/09/2016";
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
#Override
public void run() {
for (int j = 0; j < 1000; j++) {
String real = dateFormat.format(TODAY);
if (!real.equals(expected)) {
throw new RuntimeException("Mangled SimpleDateFormat");
}
}
}
}).start();
}
}
}
How can I produce a exception like NumberFormatException because I don't use a ThreadLocal ?
The crucial point is: the SimpleDateFormat implementation is not thread safe.
That doesn't mean that it will throw an exception.
It is worse: maybe, occasionally the shared formatter will simply give you wrong output!
You know, if "multi-threading issues" would nicely throw exceptions at you ... people would be much less afraid of them. Because we would have a direct hint that something went wrong.
Instead, things go wrong - and unnoticed.
Suggestion: enhance your test to
always format the same Date object
check that the result of formatting that Date is as expected (for example by comparing it against the result of an initial, first formatting operation)
And of course: only print mismatches, so that notice when they happen. Or better: throw your own exception on mismatch!
EDIT: turns out that the "better" way to enforce inconsistencies is to not use formatting but parsing!
Finally, to address another comment: of course, inconsistencies can only occur for objects that are shared between multiple threads. When each thread has its own format object, than there is no sharing; thus no problem.
Just run these code, you will get "java.lang.NumberFormatException". If not occur, run a few more times
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class ThreadLocalDemo1 implements Runnable {
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ThreadLocalDemo1 td = new ThreadLocalDemo1();
Thread t1 = new Thread(td, "Thread-1");
Thread t2 = new Thread(td, "Thread-2");
t1.start();
t2.start();
}
#Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread run execution started for " + Thread.currentThread().getName());
System.out.println("Date formatter pattern is " + simpleDateFormat.toPattern());
try {
System.out.println("Formatted date is " + simpleDateFormat.parse("2013-05-24 06:02:20"));
} catch (ParseException pe) {
pe.printStackTrace();
}
System.out.println("=========================================================");
}
}
}
Date formats are not thread safe.
I think if you format the same day you can't reproduce, you should use 2 different dates or format also second and dates that have different seconds etc. Date format uses a Calendar under the hood on which it sets the date. If the first thread sets a date and start formatting the string and another thread with a different date comes and sets it on the same calendar, you will get wrong output.
Following code produces an exception/error:
final Date today = new Date();
String expectedToday = dateFormat.format(today);
Date yesterday = new Date(today.getTime() - TimeUnit.DAYS.toMillis(1));
String expectedYesterday = dateFormat.format(yesterday);
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
#Override
public void run() {
while (true) {
String format = dateFormat.format(today);
if (!expectedToday.equals(format)) {
System.out.println("error: " + format + " " + expectedToday);//Throw exception if you want
}
format = dateFormat.format(yesterday);
if (!expectedYesterday.equals(format)) {
System.out.println("error: " + format + " " + expectedYesterday);//Throw exception if you want
}
}
}
}).start();
}
One of the ways that SimpleDateFormat is not thread safe is that it has an internal calendar field, which holds a Calendar object. Pretty much the first thing that SimpleDateFormat does before actually formatting the date is call this.calendar.setTime(theDateYouPassedIn), no synchronization or locks. I'm not sure if this is the only way, but it should be fairly straightforward to inspect the code.
So, one way to get SimpleDateFormat to fail is to use dates that will produce differing output in different threads. Here is an example:
public class NotThreadSafe
{
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
public static void main(String[] args) {
Date dref = new Date();
// Dates for yesterday and tomorrow
Date[] ds = new Date[] {
new Date(dref.getTime() - (24L * 60L * 60L * 1000L)),
new Date(dref.getTime() + (24L * 60L * 60L * 1000L))
};
String[] refs = new String[ds.length];
for (int i = 0; i < ds.length; ++i) {
// How the corresponding ds[i] should be formatted
refs[i] = dateFormat.format(ds[i]);
}
for (int i = 0; i < 100; i++) {
// Every even numbered thread uses ds[0] and refs[0],
// odd numbered threads use ds[1] and refs[1].
int index = (i % 2);
final Date d = ds[index];
final String ref = refs[index];
new Thread(new Runnable() {
#Override
public void run() {
while (true) {
String s = dateFormat.format(d);
if (!ref.equals(s)) {
throw new IllegalStateException("Expected: " + ref + ", got: " + s);
}
}
}
}).start();
}
}
}
As the comments show, every even numbered thread will format yesterday's date, and odd numbered threads will use tomorrows date.
If you run this, threads will pretty much immediately start committing suicide by throwing exceptions until such time as you have only a handful left, likely all formatting the same date.
I have to Validate a date in Java to check if it is in correct format and correct value.
If I use SimpleDateformat Class, it will make wrong date valid as well because if a month is given as 14 it will add 1 year to the Year part.
However in Oracle it will indivisually check if Month , Date , Hour , Minute etc is correct.
E.g. in Oracle
TO_DATE(20141511 , 'YYYYMMDD')
will give error that the MONTH i.e. 15 is incorrect
But in Java
Date d = "YYYYMMDD".parse("20141511");
will be valid because it will count it as 2015+3 months.
So, how can I validate a date in Java exactly like Oracle does in its TO_DATE function?
If I understand your question, you could use DateFormat.setLenient(false). Per the JavaDoc,
Specify whether or not date/time parsing is to be lenient ... With strict parsing, inputs must match this object's format.
DateFormat df = new SimpleDateFormat("yyyyMMdd");
df.setLenient(false);
try {
Date d = df.parse("20141511");
} catch (ParseException e) {
e.printStackTrace();
}
Does not allow the invalid date to parse and throws
java.text.ParseException: Unparseable date: "20141511"
None of these solutions account Oracle settings for the date format. A more global solution using oracle.sql.Date and exceptions:
import java.sql.SQLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import oracle.sql.DATE;
public void validateDate (String dateString, String nlsDateFormat, String nlsDateLanguage) throws ParseException, SQLException {
if (dateString == null) {
throw new ParseException("Date parameter not entered.", 0);
} else {
try {
DATE.fromText(dateString, nlsDateFormat, nlsDateLanguage); //not implemented in every ojdbc driver, works with ojbdbc6.jar
} catch (SQLException e) {
if (!e.getMessage().contains("Unimplemented")) {
throw new SQLException (e);
}
}
}
}
(I discovered some drivers couldn't even handle this.. so validation is bypassed if oracle.sql.DATE is not implemented)/ To get session variables for NLS_FORMAT and NLS_LANGUAGE:
private String getDateNlsFmt() throws SQLException {
String nlsDateFormat;
String sqlStmt =
"SELECT value nlsDateFormat "
+ " FROM nls_session_parameters "
+ " WHERE parameter = 'NLS_DATE_FORMAT' ";
QueryStatement q = new QueryStatement(conn, sqlStmt);
q.open();
if (!q.eof()) {
nlsDateFormat = q.getString("nlsDateFormat");
}
q.close();
return nlsDateFormat;
}
private String getDateNlsLang() throws SQLException {
String nlsDateLanguage;
String sqlStmt =
"SELECT value nlsDateLanguage "
+ " FROM nls_session_parameters "
+ " WHERE parameter = 'NLS_DATE_LANGUAGE' ";
QueryStatement q = new QueryStatement(conn, sqlStmt);
q.open();
if (!q.eof()) {
nlsDateLanguage = q.getString("nlsDateLanguage");
}
q.close();
return nlsDateLanguage;
}
I need a timezone display values as follows :
(UTC + 05:30) Chennai, Kolkata, Mumbai, New Delhi
But by using following method I am getting bit different output. How should I get the timezone display name as above ? (if required, I can use JODA).
public class TimeZoneUtil {
private static final String TIMEZONE_ID_PREFIXES =
"^(Africa|America|Asia|Atlantic|Australia|Europe|Indian|Pacific)/.*";
private static List<TimeZone> timeZones;
public static List<TimeZone> getTimeZones() {
if (timeZones == null) {
timeZones = new ArrayList<TimeZone>();
final String[] timeZoneIds = TimeZone.getAvailableIDs();
for (final String id : timeZoneIds) {
if (id.matches(TIMEZONE_ID_PREFIXES)) {
timeZones.add(TimeZone.getTimeZone(id));
}
}
Collections.sort(timeZones, new Comparator<TimeZone>() {
public int compare(final TimeZone t1, final TimeZone t2) {
return t1.getID().compareTo(t2.getID());
}
});
}
return timeZones;
}
public static String getName(TimeZone timeZone) {
return timeZone.getID().replaceAll("_", " ") + " - " + timeZone.getDisplayName();
}
public static void main(String[] args) {
timeZones = getTimeZones();
for (TimeZone timeZone : timeZones) {
System.out.println(getName(timeZone));
}
}
}
This code may do the trick for you:
public static void main(String[] args) {
for (String timeZoneId: TimeZone.getAvailableIDs()) {
TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
// Filter out timezone IDs such as "GMT+3"; more thorough filtering is required though
if (!timeZoneId.matches(".*/.*")) {
continue;
}
String region = timeZoneId.replaceAll(".*/", "").replaceAll("_", " ");
int hours = Math.abs(timeZone.getRawOffset()) / 3600000;
int minutes = Math.abs(timeZone.getRawOffset() / 60000) % 60;
String sign = timeZone.getRawOffset() >= 0 ? "+" : "-";
String timeZonePretty = String.format("(UTC %s %02d:%02d) %s", sign, hours, minutes, region);
System.out.println(timeZonePretty);
}
}
The output looks like this:
(UTC + 09:00) Tokyo
There are, however, a few caveats:
I only filter out timezones whose ID matches the format "continent/region" (e.g. "America/New_York"). You would have to do a more thorough filtering process to get rid of outputs such as (UTC - 08:00) GMT+8 though.
You should read the documentation for TimeZone.getRawOffSet() and understand what it's doing. For example, it doesn't DST effects into consideration.
On the whole, you should know that this is a messy approach, primarily because the timezone ID can be of so many different formats. Maybe you could restrict yourself down to the timezones that matter for your application, and just have a key value mapping of timezone IDs to display names?
as already explained I want to achieve, that when the user is editing a date within a JXDatePicker, he can choose, weather he types it again in the same format, which is by default dd.MM.yyyy or just dd.MM.yy. When he uses the short form I want the Picker to choose the current century.
Example:
27.01.2012 edited to 27.01.10 should result in 27.01.2010
as well as:
27.01.2012 edited to 27.01.2010 should also result in 27.01.2010
By default the JXDatePicker handels it the following way:
27.01.2012 edited to 27.01.10 results in 27.01.0010
Which is not really the way I wanted it to work. After some short research I found the following Method in SimpleDateFormat
/**
* Sets the 100-year period 2-digit years will be interpreted as being in
* to begin on the date the user specifies.
*
* #param startDate During parsing, two digit years will be placed in the range
* <code>startDate</code> to <code>startDate + 100 years</code>.
*/
public void set2DigitYearStart(Date startDate)
On first view this sounded exactly like what I need. So I tested it and unfortunatly it didnt work like I hoped it would. This is because I want to use dd.MM.yyyy as format to display dates and also want it to be displayed like that in editmode. For example when the user klicks on a date like 27.01.2012, I also want it to be like that in editmode, too and not just the short form: 27.01.12.
My Problem now is, that set2DigitYearStart(Date) unfortunatly only works, when I choose to use the shortform in editmode. I made a small example to show this case (SwingX Library is required, because of jxdatepicker and can be found be here).
public class DatePickerExample extends JPanel
{
static JFrame frame;
public DatePickerExample()
{
JXDatePicker picker = new JXDatePicker();
JTextField field = new JTextField( 10 );
add( field );
add( picker );
final Calendar instance = Calendar.getInstance();
instance.set( 2012, 01, 26 );
Date date = instance.getTime();
picker.setDate( date );
// SimpleDateFormat format = new SimpleDateFormat( "dd.MM.yy" );//Works, but I wonna display and edit it with dd.MM.yyyy
SimpleDateFormat format = new SimpleDateFormat( "dd.MM.yyyy" );
final Date startDate = new Date( 0 );//01.01.1970
format.set2DigitYearStart( startDate );
picker.setFormats( format );
}
public static void main( String[] args )
{
frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setBounds( 400, 400, 400, 400 );
frame.setLayout( new BorderLayout() );
frame.add( new DatePickerExample() );
frame.setVisible( true );
}
}
Anyone already had the same requirement and can tell me how to make this work? Any ideas are welcome. Thank you very much in advance. ymene
Final (hopefully :)
Summary of the first edit:
DatePickerFormatter already implements a lookup strategy (or CompoundFormat, as suggested by #Robin)
the lookup sequence for parsing is configurable by client code
the idea is to try parsing starting with the first (typically the "longest"), if that fails try the next (typically "not-so-long") and so on until succeeded or a parseException is thrown
for year parsing, SimpleDateFormat has rules that conflict with that longest-first lookup: it requires that "yy" is tried before "yyyy"
doing so in datePicker has the unwanted side-effect of always showing the date with the short year format
The reason is DatePickerFormatter: it doesn't allow to specify the formatting format (simply uses the first). The way out is a custom DatePickerFormatter, which supports it (in the snippet, it's hardcoded to use the second):
SimpleDateFormat longFormat = new SimpleDateFormat( "dd.MM.yyyy" );
SimpleDateFormat shortFormat = new SimpleDateFormat( "dd.MM.yy" );
Date startDate = new Date( 0 );//01.01.1970
shortFormat.set2DigitYearStart( startDate );
DatePickerFormatter formatter = new DatePickerFormatter(
// invers sequence for parsing to satisfy the year parsing rules
new DateFormat[] {shortFormat, longFormat}) {
#Override
public String valueToString(Object value) throws ParseException {
if (value == null) return null;
return getFormats()[1].format(value);
}
} ;
DefaultFormatterFactory factory = new DefaultFormatterFactory(formatter );
picker.getEditor().setFormatterFactory(factory);
Not entirely sure if we should support configuring the formatter in the base class. The DatePickerFormatter is a bit strange beast, not extending InternalFormatter and with the lookup process being a bit in competition with a FormatterFactory...
Original
It's not exactly the datePicker which handles it that way, it's the core formatting (as D1e already noted). None of the default format/ter/s support two formats at the same time: to see, try to achieve your goal with a core JFormattedTextField :-)
The way out might be a FormatterFactory: it allows to use different formats, depending on context: display and edit - the latter is used when the field is focused, the former at all other times. As the picker's editor is a JFormattedTextField, you can configure it directly (instead of using the setFormats methods)
SimpleDateFormat format = new SimpleDateFormat( "dd.MM.yyyy" );
SimpleDateFormat editFormat = new SimpleDateFormat( "dd.MM.yy" );
final Date startDate = new Date( 0 );//01.01.1970
instance.setTime(startDate);
editFormat.set2DigitYearStart( instance.getTime() );
DefaultFormatterFactory factory = new DefaultFormatterFactory(
new DatePickerFormatter(new DateFormat[] {format}),
new DatePickerFormatter(new DateFormat[] {format}),
new DatePickerFormatter(new DateFormat[] {editFormat})
);
picker.getEditor().setFormatterFactory(factory);
Edit
head banging after reading Robin's recent answer (+1!) - at last, embarassingly after years and years, I understand what SwingX' DatePickerFormatter is trying to do: that is to support a lookup chain of formatters (from longer to shorter), the longest used after committing, the shorter to ease the typing by users.
Unfortunately that doesn't work as intuitively expected. Given a sequence of formats, longer to shorter (and appropriately configured to the century):
"yyyy", "yy"
and given input
"10"
feels like being passed on from first to second, resulting in
2010
but isn't. As documented (who reads documention ... lazy me, cough ...) in SimpleDateFormat
Year: [ ... ] For parsing, if the number of pattern letters is more than 2, the year is interpreted literally, regardless of the number of digits. So using the pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
At the end of the day - as DatePickerFormatter tries to support that lookup but isn't successful - this might be considered a SwingX problem, after all :-)
I am not quite aware of JXDatePicker specifically, but if the concrete functionality you want to simulate is: Both user inputs 27.01.2010 and 27.01.10 independently should result in 27.01.2010
Then this will work:
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) throws ParseException {
String inputLiteralDateYY = "27.01.10"; //Also works with "27.01.97"
String inputLiteralDateYYYY = "27.01.2010"; //Also works with "27.01.1997"
DateFormat dfYYYY = new SimpleDateFormat("dd.MM.yyyy");
DateFormat dfYY = new SimpleDateFormat("dd.MM.yy");
Date dateFromYY = dfYY.parse(inputLiteralDateYY);
Date dateFromYYYY = dfYY.parse(inputLiteralDateYYYY);
String outputLiteralDateFromYY = dfYYYY.format(dateFromYY);
String outputLiteralDateFromYYYY = dfYYYY.format(dateFromYYYY);
System.out.println(outputLiteralDateFromYY);
System.out.println(outputLiteralDateFromYYYY);
}
}
The thing is that first you parse input with "dd.MM.yy" pattern and then return it formatting with "dd.MM.yyyy" pattern.
Hope this helps or helps applying this to your scenario.
kleopatra already explained on how to set a Format on the date picker. For this use-case, I would apply a combination of a CompositeFormat and ParseAllFormat instead of having a separate format for editing and regular mode to avoid changing the String when you start editing (as you already noticed).
Composite format
The composite format, as the name suggests, is a composite implementation of the Format class but only for the parsing. For the formatting, it uses one Format. This allows the user to input his/her date in many forms, while it is formatted consistently by using one specific format to format.
You can obtain this behavior as well by writing one more sophisticated Format. But in this case, it is easier to just use the formatting/parsing functionality offered by the SimpleDateFormat class of the JDK.
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
/**
* <p>Composite form of {#link java.text.Format Format}. It uses multiple formats for parsing, and
* only one format for formatting.</p>
*
* <p>A possible use-case is the formatting of user input (e.g. in a {#code JFormattedTextField}).
* Multiple formats for parsing allows accepting multiple forms of user input without having to
* write a complicated format.</p>
*/
public class CompositeFormat extends Format {
private List<Format> fFormats = new ArrayList<>();
private Format fFormattingFormat;
/**
* Create a new
*/
public CompositeFormat() {
}
/**
* Add a format to this composite format
*
* #param aFormat The format to add
*/
public void addFormat( Format aFormat ) {
assertNotNull( aFormat, "You cannot add a null Format" );
if ( !( fFormats.contains( aFormat ) ) ) {
fFormats.add( aFormat );
}
}
/**
* Remove a format from this composite format
*
* #param aFormat The format to remove
*/
public void removeFormat( Format aFormat ) {
assertNotNull( aFormat, "You cannot remove a null Format" );
fFormats.remove( aFormat );
updateFormattingFormat();
}
/**
* Sets <code>aFormat</code> as the format which will be used for formatting the
* objects. The format will also be added to the list of available formats.
* #param aFormat The format which will be used for formatting
*/
public void setFormattingFormat( Format aFormat ){
assertNotNull( aFormat, "Formatting format may not be null" );
addFormat( aFormat );
fFormattingFormat = aFormat;
}
private void assertNotNull( Object aObjectToCheck, String aMessage ) {
if ( aObjectToCheck == null ) {
throw new NullPointerException( aMessage );
}
}
private void updateFormattingFormat(){
if ( !( fFormats.contains( fFormattingFormat ) ) ){
fFormattingFormat = null;
if ( !( fFormats.isEmpty() ) ){
fFormattingFormat = fFormats.iterator().next();
}
}
}
#Override
public StringBuffer format( Object obj, StringBuffer toAppendTo, FieldPosition pos ) {
assertNotNull( fFormattingFormat, "Set a formatting format before using this format" );
return fFormattingFormat.format( obj, toAppendTo, pos );
}
#Override
public Object parseObject( String source, ParsePosition pos ) {
if ( fFormats.isEmpty() ){
throw new UnsupportedOperationException( "Add at least one format before using this composite format" );
}
Format formatToUse = fFormats.iterator().next();
int maxIndex = pos.getIndex();
for ( Format format : fFormats ) {
ParsePosition tempPos = new ParsePosition( pos.getIndex() );
tempPos.setErrorIndex( pos.getErrorIndex() );
format.parseObject( source, tempPos );
if ( tempPos.getIndex() > maxIndex ){
maxIndex = tempPos.getIndex();
formatToUse = format;
if( maxIndex == source.length() ){
//found a format which parses the whole string
break;
}
}
}
return formatToUse.parseObject( source, pos );
}
}
ParseAllFormat
Typically for user input you want that the whole user input can be formatted/parsed to avoid that the user can input a String which is half-correct. The ParseAllFormat is a decorator for a regular Format which throws ParseExceptions when only part of the String can be parsed.
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
/**
* <p>Decorator for a {#link Format Format} which only accepts values which can be completely parsed
* by the delegate format. If the value can only be partially parsed, the decorator will refuse to
* parse the value.</p>
*/
public class ParseAllFormat extends Format {
private final Format fDelegate;
/**
* Decorate <code>aDelegate</code> to make sure if parser everything or nothing
*
* #param aDelegate The delegate format
*/
public ParseAllFormat( Format aDelegate ) {
fDelegate = aDelegate;
}
#Override
public StringBuffer format( Object obj, StringBuffer toAppendTo, FieldPosition pos ) {
return fDelegate.format( obj, toAppendTo, pos );
}
#Override
public AttributedCharacterIterator formatToCharacterIterator( Object obj ) {
return fDelegate.formatToCharacterIterator( obj );
}
#Override
public Object parseObject( String source, ParsePosition pos ) {
int initialIndex = pos.getIndex();
Object result = fDelegate.parseObject( source, pos );
if ( result != null && pos.getIndex() < source.length() ) {
int errorIndex = pos.getIndex();
pos.setIndex( initialIndex );
pos.setErrorIndex( errorIndex );
return null;
}
return result;
}
#Override
public Object parseObject( String source ) throws ParseException {
//no need to delegate the call, super will call the parseObject( source, pos ) method
return super.parseObject( source );
}
}
The combination of these both classes allows for the following code
import java.text.Format;
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class FormattingDemo {
private static Format createCompositeDateFormat(){
Format formattingFormat = new ParseAllFormat( new SimpleDateFormat( "dd.MM.yyyy" ) );
SimpleDateFormat shortFormat = new SimpleDateFormat( "dd.MM.yy" );
Format otherFormat = new ParseAllFormat( shortFormat );
CompositeFormat compositeFormat = new CompositeFormat();
compositeFormat.addFormat( otherFormat );
compositeFormat.addFormat( formattingFormat );
compositeFormat.setFormattingFormat( formattingFormat );
return compositeFormat;
}
public static void main( String[] args ) throws ParseException {
Format dateFormat = createCompositeDateFormat();
System.out.println( dateFormat.parseObject( "27.01.2010" ) );
System.out.println( dateFormat.parseObject( "27.01.10" ) );
System.out.println( dateFormat.parseObject( "27.01.2012" ) );
System.out.println(dateFormat.format( dateFormat.parseObject( "27.01.2010" ) ));
System.out.println(dateFormat.format( dateFormat.parseObject( "27.01.10" ) ));
System.out.println(dateFormat.format( dateFormat.parseObject( "27.01.2012" ) ));
}
}
resulting in the following output
Wed Jan 27 00:00:00 CET 2010
Wed Jan 27 00:00:00 CET 2010
Fri Jan 27 00:00:00 CET 2012
27.01.2010
27.01.2010
27.01.2012
Note that there is a small catch for which I did not found a decent solution. The order in which you add Format instances to the CompositeFormat is also the order in which they are evaluated for the parsing. In this case you need to add them in the correct order as even the new SimpleDateFormat( "dd.MM.yyyy" ) seems to accept the input string 27.01.10 and can parse the whole String to a Date object equivalent to 27.01.0010.