JXDatePicker using SimpleDateFormat to format dd.MM.yy to dd.MM.yyyy with current century

本小妞迷上赌 提交于 2019-11-29 04:48:05

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!