概要

DateCalendarなどを使用するSpinnerDateModelの代わりに、JDK 8で導入されたLocalDateTimeなどを使用するSpinnerModelを作成して、JSpinnerで日付を選択します。

サンプルコード

class SpinnerLocalDateTimeModel extends AbstractSpinnerModel implements Serializable {
  private Comparable<ChronoLocalDateTime<?>> start, end;
  private ChronoLocalDateTime<?> value;
  private TemporalUnit temporalUnit;

  public SpinnerLocalDateTimeModel(
      ChronoLocalDateTime<?> value,
      Comparable<ChronoLocalDateTime<?>> start,
      Comparable<ChronoLocalDateTime<?>> end,
      TemporalUnit temporalUnit) {
    super();
    if (Objects.nonNull(start) && start.compareTo(value) >= 0
        || Objects.nonNull(end) && end.compareTo(value) <= 0) {
      throw new IllegalArgumentException("(start <= value <= end) is false");
    }
    this.value = Optional.ofNullable(value)
        .orElseThrow(() -> new IllegalArgumentException("value is null"));
    this.start = start;
    this.end = end;
    this.temporalUnit = temporalUnit;
  }

  public void setStart(Comparable<ChronoLocalDateTime<?>> start) {
    if (Objects.isNull(start) ? Objects.nonNull(this.start)
                              : !Objects.equals(start, this.start)) {
      this.start = start;
      fireStateChanged();
    }
  }

  public Comparable<ChronoLocalDateTime<?>> getStart() {
    return start;
  }

  public void setEnd(Comparable<ChronoLocalDateTime<?>> end) {
    if (Objects.isNull(end) ? Objects.nonNull(this.end)
                            : !Objects.equals(end, this.end)) {
      this.end = end;
      fireStateChanged();
    }
  }

  public Comparable<ChronoLocalDateTime<?>> getEnd() {
    return end;
  }

  public void setTemporalUnit(TemporalUnit temporalUnit) {
    if (temporalUnit != this.temporalUnit) {
      this.temporalUnit = temporalUnit;
      fireStateChanged();
    }
  }

  public TemporalUnit getTemporalUnit() {
    return temporalUnit;
  }

  @Override public Object getNextValue() {
    // Calendar cal = Calendar.getInstance();
    // cal.setTime(value.getTime());
    // cal.add(calendarField, 1);
    // Date next = cal.getTime();
    ChronoLocalDateTime<?> next = value.plus(1, temporalUnit);
    return Objects.isNull(end) || end.compareTo(next) >= 0 ? next : null;
  }

  @Override public Object getPreviousValue() {
    // Calendar cal = Calendar.getInstance();
    // cal.setTime(value.getTime());
    // cal.add(calendarField, -1);
    // Date prev = cal.getTime();
    ChronoLocalDateTime<?> prev = value.minus(1, temporalUnit);
    return Objects.isNull(start) || start.compareTo(prev) <= 0 ? prev : null;
  }

  public ChronoLocalDateTime<?> getLocalDateTime() {
    return value;
  }

  @Override public Object getValue() {
    return value;
  }

  @Override public void setValue(Object value) {
    if (!(value instanceof ChronoLocalDateTime<?>)) {
      throw new IllegalArgumentException("illegal value");
    }
    if (!value.equals(this.value)) {
      this.value = (LocalDateTime) value;
      fireStateChanged();
    }
  }
}
View in GitHub: Java, Kotlin

解説

  • 上: SpinnerDateModel
    • Calendarなどを使用して開始日、終了日を生成しSpinnerDateModelを作成してJSpinnerに設定
      Calendar cal = Calendar.getInstance();
      cal.clear(Calendar.MILLISECOND);
      cal.clear(Calendar.SECOND);
      cal.clear(Calendar.MINUTE);
      cal.set(Calendar.HOUR_OF_DAY, 0);
      Date date = cal.getTime();
      cal.add(Calendar.DATE, -2);
      Date start = cal.getTime();
      cal.add(Calendar.DATE, 9);
      Date end = cal.getTime();
      JSpinner spinner1 = new JSpinner(new SpinnerDateModel(
          date, start, end, Calendar.DAY_OF_MONTH));
      
    • 参考: JSpinnerで日付を設定
  • 下: SpinnerLocalDateTimeModel
    • 内部でDateではなくLocalDateTimeを使用するSpinnerLocalDateTimeModel(AbstractSpinnerModelを継承)を作成
      LocalDateTime d = LocalDateTime.now();
      LocalDateTime s = d.minus(2, ChronoUnit.DAYS);
      LocalDateTime e = d.plus(7, ChronoUnit.DAYS);
      JSpinner spinner2 = new JSpinner(new SpinnerLocalDateTimeModel(
          d, s, e, ChronoUnit.DAYS));
      
    • スピナエディタも以下のようなLocalDateTimeを扱うものに変更
      • 参考: Temporal Spinners « Java Tips Weblog
        class LocalDateTimeEditor extends JSpinner.DefaultEditor {
          private final DateTimeFormatter dateTimeFormatter;
          private final SpinnerLocalDateTimeModel model;
        
          public LocalDateTimeEditor(final JSpinner spinner, String dateFormatPattern) {
            super(spinner);
            if (!(spinner.getModel() instanceof SpinnerLocalDateTimeModel)) {
              throw new IllegalArgumentException("model not a SpinnerLocalDateTimeModel");
            }
            dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormatPattern);
            model = (SpinnerLocalDateTimeModel) spinner.getModel();
            final DefaultFormatter formatter = new LocalDateTimeFormatter();
        
            EventQueue.invokeLater(new Runnable() {
              @Override public void run() {
                formatter.setValueClass(LocalDateTime.class);
                DefaultFormatterFactory factory = new DefaultFormatterFactory(formatter);
                JFormattedTextField ftf = (JFormattedTextField) getTextField();
                try {
                  String maxString = formatter.valueToString(model.getStart());
                  String minString = formatter.valueToString(model.getEnd());
                  ftf.setColumns(Math.max(maxString.length(), minString.length()));
                } catch (ParseException e) {
                  // PENDING: hmuller
                  e.printStackTrace();
                }
                ftf.setHorizontalAlignment(JTextField.LEFT);
                ftf.setEditable(true);
                ftf.setFormatterFactory(factory);
              }
            });
          }
        
          public SpinnerLocalDateTimeModel getModel() {
            return (SpinnerLocalDateTimeModel) getSpinner().getModel();
          }
        
          class LocalDateTimeFormatter extends InternationalFormatter {
            public LocalDateTimeFormatter() {
              super(dateTimeFormatter.toFormat());
            }
        
            @Override public String valueToString(Object value) throws ParseException {
              // System.out.println(value.getClass().getName());
              if (value instanceof TemporalAccessor) {
                // return ((LocalDateTime) value).format(dateTimeFormatter);
                return dateTimeFormatter.format((TemporalAccessor) value);
              } else {
                return "";
              }
            }
        
            @Override public Object stringToValue(String text) throws ParseException {
              // System.out.println("stringToValue:" + text);
              try {
                // LocalDateTime value = LocalDate.parse(text, dateTimeFormatter).atStartOfDay();
                TemporalAccessor ta = dateTimeFormatter.parse(text);
                ChronoLocalDateTime<?> value = model.getLocalDateTime();
                // @see https://tips4java.wordpress.com/2015/04/09/temporal-spinners/
                for (ChronoField field : ChronoField.values()) {
                  if (field.isSupportedBy(value) && ta.isSupported(field)) {
                    value = field.adjustInto(value, ta.getLong(field));
                  }
                }
                Comparable<ChronoLocalDateTime<?>> min = model.getStart();
                Comparable<ChronoLocalDateTime<?>> max = model.getEnd();
                if (Objects.nonNull(min) && min.compareTo(value) > 0
                 || Objects.nonNull(max) && max.compareTo(value) < 0) {
                  throw new ParseException(text + " is out of range", 0);
                }
                return value;
              } catch (DateTimeParseException e) {
                ParseException pe = new ParseException(e.getMessage(), e.getErrorIndex());
                pe.setStackTrace(e.getStackTrace());
                throw pe;
              }
            }
          }
        }
        

  • JDK-8169482 java.time.DateTimeFormatter javadoc: F is not week-of-month - Java Bug System
  • time - Java 8 DateFormatter week of month and year key symbols difference - Stack Overflow
    • DateTimeFormatterのパターン文字で、WFの説明が両方week-of-monthになっている
    • 実際はFday_of_week_in_monthで、JDK 1.9.0ではドキュメントが修正されている
  • day_of_week_in_monthについて
    • ChronoField#ALIGNED_DAY_OF_WEEK_IN_MONTH (Java Platform SE 8)
      たとえば、7日からなる週を使用する暦体系では、最初の「位置合せされた月の週番号」は「月の日」1から始まり、2番目の「位置合せされた月の週番号」は「月の日」8から始まる、というようになります。これらの位置合せされた各週に含まれる日に1から7までの番号が付けられ、このフィールドの値として返されます。したがって、「月の日」1から7では、「位置合せされた曜日」の値は1から7になります。また、「月の日」8から14でもこれが繰り返され、「位置合せされた曜日」の値は1から7になります。
    • F(day_of_week_in_month)は1から7の数値文字列に変換される
    • 例えば月の第一日が日曜日の場合、その月の日曜日は1、月曜日は2、土曜日は7となる
      • 2017-01-10(火)は2017-01-01が日曜なので3に変換される
      • DateTimeFormatter.ofPattern("F").format(LocalDate.of(2017, Month.JANUARY, 10))は文字列の3を返す

参考リンク

コメント