Skip to content

Commit

Permalink
#201 done (#202)
Browse files Browse the repository at this point in the history
DatePattern now does not need a separator (but requires zero-prefixed day and month in that case)
  • Loading branch information
vaadin-miki authored Jul 24, 2020
1 parent 21a4e7e commit e382394
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,11 @@ private void buildItemGrid(Component component, Consumer<Component[]> callback)
}

private void buildHasDatePattern(Component component, Consumer<Component[]> callback) {
final ComboBox<DatePattern> patterns = new ComboBox<>("Select date display pattern:", DatePatterns.YYYY_MM_DD, DatePatterns.M_D_YYYY_SLASH, DatePatterns.DD_MM_YYYY_DOTTED, DatePatterns.D_M_YY_DOTTED);
final ComboBox<DatePattern> patterns = new ComboBox<>("Select date display pattern:",
DatePatterns.YYYY_MM_DD, DatePatterns.M_D_YYYY_SLASH,
DatePatterns.DD_MM_YYYY_DOTTED, DatePatterns.D_M_YY_DOTTED,
DatePatterns.YYYYMMDD, DatePatterns.DDMMYY
);
final Button clearPattern = new Button("Clear pattern", event -> ((HasDatePattern)component).setDatePattern(null));
clearPattern.setDisableOnClick(true);
final Component clearPatternOrContainer;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.vaadin.miki.shared.dates;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.Objects;

Expand All @@ -17,9 +20,21 @@ public class DatePattern implements Serializable {
*/
public enum Order {DAY_MONTH_YEAR, MONTH_DAY_YEAR, YEAR_MONTH_DAY}

/**
* Shorthand for no separator (character 0).
*/
public static final char NO_SEPARATOR = 0;

/**
* Default separator, {@code -}.
*/
public static final char DEFAULT_SEPARATOR = '-';

private static final Logger LOGGER = LoggerFactory.getLogger(DatePattern.class);

private final String displayName;

private char separator = '-';
private char separator = DEFAULT_SEPARATOR;

private boolean zeroPrefixedDay = true;

Expand Down Expand Up @@ -58,25 +73,49 @@ public char getSeparator() {
return separator;
}

/**
* Checks whether or not there is a separator present.
* @return Whether or not {@link #getSeparator()} returns something else than {@link #NO_SEPARATOR}.
*/
public boolean hasSeparator() {
return this.getSeparator() != NO_SEPARATOR;
}

/**
* Sets new separator.
* If the separator is {@link #NO_SEPARATOR} (zero), zero-prefixed month and zero-prefixed day will be automatically enabled.
* @param separator Separator between parts.
*/
public void setSeparator(char separator) {
this.separator = separator;
if(separator == NO_SEPARATOR) {
this.withZeroPrefixedDay(true).setZeroPrefixedMonth(true);
LOGGER.warn("disabling date pattern separator, turning on zero-prefixed day and zero-prefixed month");
}
}

/**
* Chains {@link #setSeparator(char)} and returns itself.
* @param separator Separator.
* @return This.
* @see #setSeparator(char)
* @see #withoutSeparator()
*/
public DatePattern withSeparator(char separator) {
this.setSeparator(separator);
return this;
}

/**
* Identical to {@code withSeparator(DatePattern.NO_SEPARATOR}.
* @return This.
* @see #withSeparator(char)
* @see #setSeparator(char)
*/
public DatePattern withoutSeparator() {
return this.withSeparator(NO_SEPARATOR);
}

/**
* Checks whether days should be prefixed with {@code 0}.
* @return Whether or not days will be zero-prefixed ({@code 09} instead of {@code 9}); {@code true} by default.
Expand All @@ -87,10 +126,15 @@ public boolean isZeroPrefixedDay() {

/**
* Sets whether or not days should be prefixed with {@code 0}.
* When there is no separator and this flag is turned off, the separator will be set to {@link #DEFAULT_SEPARATOR}.
* @param zeroPrefixedDay When {@code true} and day is one digit, zero will be added in front of that number.
*/
public void setZeroPrefixedDay(boolean zeroPrefixedDay) {
this.zeroPrefixedDay = zeroPrefixedDay;
if(!zeroPrefixedDay && !this.hasSeparator()) {
this.setSeparator(DEFAULT_SEPARATOR);
LOGGER.warn("turning off zero-prefixed day requires a separator, setting it to be the default one ({})", DEFAULT_SEPARATOR);
}
}

/**
Expand Down Expand Up @@ -118,6 +162,10 @@ public boolean isZeroPrefixedMonth() {
*/
public void setZeroPrefixedMonth(boolean zeroPrefixedMonth) {
this.zeroPrefixedMonth = zeroPrefixedMonth;
if(!zeroPrefixedMonth && !this.hasSeparator()) {
this.setSeparator(DEFAULT_SEPARATOR);
LOGGER.warn("turning off zero-prefixed month requires a separator, setting it to be the default one ({})", DEFAULT_SEPARATOR);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,41 @@ public final class DatePatterns {
/**
* Uses zero-prefixed day and month, full year, separated by {@code .}.
*/
public static final DatePattern DD_MM_YYYY_DOTTED = new DatePattern("dd.MM.yyyy").
withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR).
withSeparator('.');
public static final DatePattern DD_MM_YYYY_DOTTED = new DatePattern("dd.MM.yyyy")
.withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR)
.withSeparator('.');

/**
* Uses day, month and short year with century boundary year 40 (years less than 40 are from 21st century), separated by {@code .}.
*/
public static final DatePattern D_M_YY_DOTTED = new DatePattern("d.M.yy").
withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR).
withShortYear(true).
withSeparator('.').withZeroPrefixedDay(false).withZeroPrefixedMonth(false).
withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false);
public static final DatePattern D_M_YY_DOTTED = new DatePattern("d.M.yy")
.withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR)
.withShortYear(true)
.withSeparator('.').withZeroPrefixedDay(false).withZeroPrefixedMonth(false)
.withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false);

/**
* Uses month, day and full year, separated by {@code /}.
*/
public static final DatePattern M_D_YYYY_SLASH = new DatePattern("M/d/yyyy").
withDisplayOrder(DatePattern.Order.MONTH_DAY_YEAR).
withSeparator('/').withZeroPrefixedDay(false).withZeroPrefixedMonth(false);
public static final DatePattern M_D_YYYY_SLASH = new DatePattern("M/d/yyyy")
.withDisplayOrder(DatePattern.Order.MONTH_DAY_YEAR)
.withSeparator('/').withZeroPrefixedDay(false).withZeroPrefixedMonth(false);

/**
* Uses full year, zero-prefixed month and day, and no separator.
*/
public static final DatePattern YYYYMMDD = new DatePattern("yyyyMMdd")
.withDisplayOrder(DatePattern.Order.YEAR_MONTH_DAY)
.withSeparator(DatePattern.NO_SEPARATOR);

/**
* Uses zero-prefixed day and month with short year (century boundary year 40, years less than 40 in 21st century), and no separator.
*/
public static final DatePattern DDMMYY = new DatePattern("ddMMyy")
.withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR)
.withSeparator(DatePattern.NO_SEPARATOR)
.withShortYear(true)
.withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false);

private DatePatterns() {} // instances not needed
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ private static String convertDatePatternToClientPattern(DatePattern pattern) {
final String yearPart = pattern.isShortYear() ? "0y" : "_y";

StringBuilder builder = new StringBuilder();
builder.append(pattern.getSeparator());
if(pattern.hasSeparator())
builder.append(pattern.getSeparator());
switch (pattern.getDisplayOrder()) {
case DAY_MONTH_YEAR:
builder.append(dayPart).append(monthPart).append(yearPart);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ export class DatePatternMixin {
return
}
console.log('SDP: MAIN - set display pattern');
// pattern is a string, first character is a separator
// the pattern parts:
// - separator (1) - optional
// - day, month, year (3 x 2 = 6) - required
// - century indicator, default century and boundary year (1 + 2 + 2 = 5) - optional
// this gives: 7 or 12 characters when the separator is present, 6 or 11 when it is not, and 0/null for none

// pattern is a string, first optional character is a separator
// next six characters define, in groups of 2, the order and what should be displayed
// 0d or _d - (zero prefixed) day
// 0M or _M - (zero prefixed) month
// 0y or _y - short year or full year
// the remaining of the pattern is optional, only when yy is used
// the remaining of the pattern is optional, only when 0y is used
// + or - - corresponds to previous century in short year (+ when true, - when false)
// XX - number corresponding to the default century in short years: MUST BE TWO DIGITS
// YY - number corresponding to the boundary year: MUST BE TWO DIGITS
Expand All @@ -57,8 +63,9 @@ export class DatePatternMixin {
}
datepicker.set('i18n.formatDate', date => {
const ddp = datepicker.i18n.dateDisplayPattern;
const startIndex = (ddp.length === 7 || ddp.length === 12) ? 1 : 0;
console.log('SDP: custom formatting for ' + ddp);
return [ddp.substr(1, 2), ddp.substr(3, 2), ddp.substr(5, 2)].map(part => {
return [ddp.substr(startIndex, 2), ddp.substr(startIndex+2, 2), ddp.substr(startIndex+4, 2)].map(part => {
if (part === '0d') {
return String(date.day).padStart(2, '0')
} else if (part === '_d') {
Expand All @@ -72,40 +79,88 @@ export class DatePatternMixin {
} else if (part === '_y') {
return String(date.year)
}
}).join(ddp[0]);
}).join(startIndex === 1 ? ddp[0] : '');
});
datepicker.set('i18n.parseDate', text => {
const ddp = datepicker.i18n.dateDisplayPattern;
const shortYear = ddp.indexOf('0y') !== -1;
console.log('SDP: custom parsing for ' + ddp);
const today = new Date();
let date, month = today.getMonth(), year = today.getFullYear();
const parts = text.split(ddp[0]);
if (parts.length === 3) {
// d, M, y can be at index 2, 4 or 6 in the pattern
year = parseInt(parts[(ddp.indexOf('y') / 2) - 1]);
month = parseInt(parts[(ddp.indexOf('M') / 2) - 1]) - 1;
date = parseInt(parts[(ddp.indexOf('d') / 2) - 1]);
// now, if short year is used
if (ddp.indexOf('0y') !== -1) {
const boundaryYear = parseInt(ddp.substr(-2));
const defaultCentury = parseInt(ddp.substr(-4, 2));
if (year < boundaryYear) {
year += (ddp[7] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100;
} else if (year < 100) {
year += (ddp[7] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100;
// this part is triggered when there is a separator
if (ddp.length === 7 || ddp.length === 12) {
const parts = text.split(ddp[0]);
if (parts.length === 3) {
// d, M, y can be at index 2, 4 or 6 in the pattern
year = parseInt(parts[(ddp.indexOf('y') / 2) - 1]);
month = parseInt(parts[(ddp.indexOf('M') / 2) - 1]) - 1;
date = parseInt(parts[(ddp.indexOf('d') / 2) - 1]);
} else if (parts.length === 2) {
if (ddp.indexOf('d') < ddp.indexOf('M')) {
date = parseInt(parts[0]);
month = parseInt(parts[1]) - 1;
} else {
date = parseInt(parts[1]);
month = parseInt(parts[0]) - 1;
}
}
} else if (parts.length === 2) {
if (ddp.indexOf('d') < ddp.indexOf('M')) {
} else if (parts.length === 1) {
date = parseInt(parts[0]);
month = parseInt(parts[1]) - 1;
} else {
date = parseInt(parts[1]);
month = parseInt(parts[0]) - 1;
}
} else if (parts.length === 1) {
date = parseInt(parts[0]);
}
// there is no separator and thus by definition month and day are zero-based
// this means the pattern starts directly with day/month/year parts, each taking two characters
// it also means that the input is composed of parts with two eventually up to four characters
else {
const dayOrder = Math.floor(ddp.indexOf('d') / 2);
const monthOrder = Math.floor(ddp.indexOf('M') / 2);
const yearOrder = Math.floor(ddp.indexOf('y') / 2);
// if the length of the input is 1 or 2, it is a day
if (text.length <= 2) {
date = parseInt(text);
}
// length being 3 or 4 means there is a day and a month, in order defined by the pattern
else if (text.length <= 4) {
// day first
if (dayOrder < monthOrder) {
date = parseInt(text.substr(0, 2));
month = parseInt(text.substr(2)) - 1;
}
// month first
else {
month = parseInt(text.substr(0, 2));
date = parseInt(text.substr(2)) - 1;
}
}
// length is more than 4 characters, which means there is also year involved
else {
let yearPosition = yearOrder * 2;
let dayPosition = dayOrder * 2;
let monthPosition = monthOrder * 2;
// if year is full, month and day can potentially be offset by 2 extra characters
if (!shortYear) {
if (dayOrder > yearOrder)
dayPosition += 2;
if (monthOrder > yearOrder)
monthPosition += 2;
}
date = parseInt(text.substr(dayPosition, 2));
month = parseInt(text.substr(monthPosition, 2)) - 1;
year = parseInt(text.substr(yearPosition, shortYear ? 2 : 4));
}
}
// end of parsing stuff

// now, if short year is used
if (shortYear) {
const boundaryYear = parseInt(ddp.substr(-2));
const defaultCentury = parseInt(ddp.substr(-4, 2));
if (year < boundaryYear) {
year += (ddp[ddp.length-5] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100;
} else if (year < 100) {
year += (ddp[ddp.length-5] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100;
}
}
// return result
if (date !== undefined) {
return {day: date, month, year};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class SuperDatePicker extends TextSelectionMixin.to(DatePatternMixin.to(DatePick

setCallingServer(callingServer) {
console.log('SDP: configuring text selection listeners; callingServer flag is '+callingServer);
this.listenToEvents(this.shadowRoot.querySelector('vaadin-text-field').inputElement, this, callingServer);
this.listenToEvents(this.shadowRoot.querySelector('vaadin-date-picker-text-field').inputElement, this, callingServer);
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.vaadin.miki.shared.dates;

import org.junit.Assert;
import org.junit.Test;

public class DatePatternTest {

@Test
public void noSeparatorMeansZeroPrefixedDayAndMonth() {
final DatePattern pattern = new DatePattern().withZeroPrefixedDay(false).withZeroPrefixedMonth(false);
Assert.assertTrue(pattern.hasSeparator());
pattern.withoutSeparator();
Assert.assertFalse(pattern.hasSeparator());
Assert.assertTrue("zero prefixed day must be set when there is no separator", pattern.isZeroPrefixedDay());
Assert.assertTrue("zero prefixed month must be set when there is no separator", pattern.isZeroPrefixedMonth());
}

@Test
public void turningOffZeroPrefixedDaySetsDefaultSeparatorWhenWasNone() {
final DatePattern pattern = new DatePattern().withoutSeparator();
Assert.assertFalse(pattern.hasSeparator());
pattern.setZeroPrefixedDay(false);
Assert.assertEquals("separator should be reverted to default", DatePattern.DEFAULT_SEPARATOR, pattern.getSeparator());
}

@Test
public void turningOffZeroPrefixedMonthSetsDefaultSeparatorWhenWasNone() {
final DatePattern pattern = new DatePattern().withoutSeparator();
Assert.assertFalse(pattern.hasSeparator());
pattern.setZeroPrefixedMonth(false);
Assert.assertEquals("separator should be reverted to default", DatePattern.DEFAULT_SEPARATOR, pattern.getSeparator());
}

}

0 comments on commit e382394

Please sign in to comment.