A Concept for Fast & More Flexible Date Entry

0
205

Introduction

I have been working on the concept of a way of entering dates with minimum keystrokes for a number of years, and have implemented some of the ideas in some applications I have built. Why should you have to enter a month when you all you should have to do is specify the day of the month? What I have used this concept for is entering dates for purchases, and those only happen in the past, so that if you enter a day or a month and a day, therefore I used an implementation that defaults to dates in the past, and not to dates in the future. Thus why should you have to put the year in even when it is January and you want to specify a day in December. Why can you not specify a holiday, and have the date automatically added.

Another thing I discovered was that it was quite often easier to enter a day of the week since I quite often did not know the day but only the day of the week, and specifying a day of the week in English would require only one or two characters.

Other ideas were added to the concept as I proceeded with creating a more robust design that was easy to extend, and provided a way to handle different languages..

The Basic Concept

Numeric Input

It is well known that the fastest way of entering information is when the user leaves his hands on the keyboard. I was not particularly happy with entering dates since it quite often required the user to enter a lot more keystrokes than were necessary. Why not allow the user to just enter a number, and determine the date from that number; if the user enters a two digit number less than 31, that could (normally) be considered the day of the month. Actually I went a little bit further than that and created a design that would assume that the number was the day of the month before the current date so if today was the 16th and the user entered ‘20’; then it was assumed to that the entered value was the 20th of the previous month. It would also possible to assume that the day would be a future date, or the day that was closest to the current date. The design can also accommodate preferring dates in the future.

Obviously, numbers larger than 31 (or larger than the days in the assumed month), and not too large, could be assumed to be a month-day combination (or a day-month combination). Thus if the user entered ‘91’, then an obviously assumption is that the user was entering September 1st since there is never a 91st day of the month. This date could also be entered as ‘901’. If the user entered ‘111’, this could either be January 11th or November 1st. The safe assumption would be January 1st and just require a 4 digit entry when there is a two digit month to force the month. The other possibility is to assume the date closest to the current date.

The year is handled somewhat similarly to the month. A 6 digit would be a ‘mmddyy’ entry with a two digit year and an 8 digit entry would be a ‘mmddyyyy’ entry with a four digit year. The design actually handles a lot more combinations because the obvious is attempted first, and if that is an invalid date, then other possibilities is attempted, including combinations with the day followed by the month.

Part of the concept is the ability to enter a number without any space and the date being determined from that number. Obviously if a date is entered with separated numbers, the job is much easier, but the entry could mix up the order from what is expected; the code still tries to create a valid date. If a number entered is less than or equal to the days in the month, then the code assumes that the number represents a day. As the numbers get larger, then there is more decision making involved. An entry like “66” is easily converter to June 6th and 812 is converted to August 12th, but “111” could be January 11th or November 1st. A number like 0402 would be best translated to April 2nd, but what about “9121”. Then there are numbers entered with a month that is specified by spelling out the month or its abbreviation. If the number is too big to be the day of the month, then need to see if the number represents a year and a day.

Letters

In addition to using a number of the month, allowing the user to enter the month name, including abbreviations where only part of the month is specified. A later idea was to allow the month to be specified with a number and no spaces such that “N1” would be translated as November 1st. This helps a lot since “111” could be January 1st. Of course some months start with the same letter, so multiple letters are required to distinguish between two different months, particularly the letter “J” is the first letter for several months in English. Fortunately most of the months that could be confused with a numeric input (i.e., February, March, October, November, December) start with unique letter in English. The only issue is January, and for that entering just the letter “J” becomes January, with both “July” and “June” requiring at least 3 letters.

Using these two concepts, most date entries can be done with an entry of no more than 3 characters.

Another useful entry option is to be able to use a day of the week or abbreviated day of the week. If the user enters ‘fr’, then the code will return the date for the previous Friday (3 days previous). I have found this very useful since I know that the date of interest was a day of the week, but do not know the day of the month associated with that day of the week.

Then there was extending the design to allow the user to enter the day of the week, and to provide a way to specify which day of the month or how many weeks into the past or future (using a number with a sign). Then there was the ability to enter holidays using the name or an abbreviation.

The Design

There are basically three parts of the code that handle different aspects of the decoding of the user input, and there is a XML file that is used to handle differences in names (and holidays) in different languages, and to specify abbreviations.

The Order Preference

The order preference basically handles the default order that the Day, Month, and Year are entered. In different parts of the world the default order is different. In the US is it Month-Day-Year, but most of the world it is Day-Month-Year and in China it is Year-Month-Day (which I think is the best ordering). To do this there will be a separate class to handle each ordering because there is a lot of decision making to be done, especially if the input is a single number. Part of the decision making is that if the default does not result in a valid date, then the code attempts a different order. All the order preference classes derive from the abstract class abstractOrderPreference. Currently I have only implemented the class OrderPreferenceMDY, which attempts to find a date in the default Month-Day-Year order. By specifying the right Order Preference class, the cultural differences in date entry can be accommodated.

Most of the code in the Order Preferences classes handles entering a date as a string of up to 8 digits (or up to 6 digits and a month). The code is simplified by use of a private class called ParseDate whose constructor takes two strings—one that specifies whether a position in the other string is to be part of Month, Day or Year.

Two properties must be set after an Order Preference class is initialized: a RelativeDate and the CreateDate delegate.

The RelativeDate is needed by the abstractOrderPreference to differentiate sometimes between two options like when “111” is entered. This can be either January 11th or November 1st. Having the month available allows the closest date to be selected.

An important aspect of the Order Preference calculation is the delegate CreateDateDelegate. This delegate has arguments for the month, day, and year, and is responsible for adjusting the Date for according to preference for when the date entry is incomplete such as missing the month and year or just the year.

The Date Relative

The completion of the date when information is missing such as month and year or just year is resolved with classes derived from the abstract class abstractDateResolver. The method is resolution is specified by selecting the class derived from this abstract class. Currently only one class has been developed which will resolve to the past:

ResolveToPast.

Resolving to the past that if today is the 12th and the value entered is “13”, then the date will be resolved to the 13th of the previous month since preference is to the past. If today was March 13th and the entered value resolves to November 15th with no year specified, then the resolved date will be November 15th of the previous year.

Other potential resolvers would be to a day in the future, or the closest date.

The constructor for a Date Resolver takes the date to use as the base date (e.g., if the class is ResolveToPast, then the date from witch anything considered in the past is before this date and anything to be considered the future is after this date), and an instance of the abstractOrderPreference class. In the Date Resolver base class constructor, the abstractOrderPreference class property RelativeDate is set to the date passed to the constructor, and the CreateDate delegate property is set to the CreateDate method in the derived class.

The Parser

There are basically two types of user input that is processed when scanned: keywords and numbers. The parser depends on a dictionary of keywords that is used to determine if part of the user input is a keyword. Keywords include months, days of the week, and other key words. From these keyword, user input tokens are created. There is also a second type of token created, and that are ones created from numeric input. A number without a sign is assumed to specify at least some part of the date while a number with a “+” or “-“ sign specifies a relative value that is used to change the date created from the other tokens by that amount, which currently can be weeks (normally only when a day of the week keyword is found) or days. Each of these keywords has an associated type:

  • Month: there is one of these for each month of the year
  • Day: there is one of these for each day of the week
  • Multiple: used to allow a keyword to be associated with more than one keyword type, for instance “F” could be “Friday” or “February.” The other tokens created by the parser determine which is used.
  • Ordinal: this is generally used for the week of the month: First (1st), Second (2nd), Third (3rd), Fourth (4th) and Last. It allows the user to specify the nth week of a month.
  • Relative: allows a keyword to be associated with a day relative to the relative date. For instance if today it the 3rd, and the user input is “Yesterday” then the 2nd of this month would be returned.
  • Special: this allows holidays to be specified and currently supports entries for the week of the month, the day of the week, the day of the month, and the month.

When the user input is processed, a keyword dictionary using the above token types are used to help generate the user input tokens. In addition to using creating tokens based on the keyword types above, and user numeric input with a plus or minus sign is encapsulated into a “relative” token and any other number is encapsulated in a “number” token.

The constructor for the class DateProcessor initializes the environment and provides a Parse command to return the date.

The Parse method of the DateProcessor is used to convert the user input string to the date. The method splits the string into words, and these words use the KeywordDictionary class to find the KeywordToken that should be associated with the word. Then this data is used to create a UserInputToken with the TokenProcessor AddToken method. After processing each word and adding to the TokenProcessor, the TokenProcessor GetDate method is called, and the result is returned.

When initialized, the KeywordDictionary creates the KeywordDictionary using XmlDocument provided in the constructor. It only has one public method, FindKeyword with the word passed as a string. The find keyword check the list of Keywords for a match with the word, and return the result. It will also associated any numeric values with the appropriate keyword type, and return that result.

The XML File

A lot of the information used for parsing is contained in an XML file. The following shows examples of the data in the XML file:

<items>
 <item type="month" value="3">MAR[CH]</item>
 <item type="day" value="2">MO[NDAY]</item>
 <item type="multiple" value="DAYOFWEEK:2 MONTH:3">M</item>
 <item type="ordinal" value="1">FI[RST]</item>
 <item type="relative" value="-1">Y[ESTERDAY]</item>
 <item type="special" value="LAST DAYOFWEEK:2 MONTH:5">MEM[ORIAL DAY]</item>
</items>

Currently there is only the one file, but the idea is to have a file for each language supported. Only a few of the entries are show above. It should be noticed that there is a multiple type entry that includes day of the week and month for the value “M”, and month entry for March and a day of the week entry for Monday. This multiple entry allows the single letter “M” to represent either March or Monday, and which depends on other tokens created.

The Entry Point

The DateProcessor class is the only public class in the project. It currently has a single constructor that uses enumerations specify the Order Preference and the way incomplete dates are to be resolved. The class is used for Order Preference and Date Resolution classes used are based on these enumerations. This constructor that uses these enumerations, a path to find the XML file that will contain the keywords, and the date to use as the base date for the Order Preference.

The date is found by calling the public Parse method, passing a string that is to be processed to determine a date as the single argument in the method.

Implemented Capability

The types of input following has been implemented:

  • Numeric: Numeric entry of up to 8 digits, either as a continuous set of digits or broken up into separate pieces for month, day, and year (still will handle two of these combined). Examples of entry follow:

    • “12”: 12th day of the month

    • “31”: 31st day of the month if the month has 31 days, or March 1st.

    • “66”: June 6th since there is no month with 66 days.

    • “1111”: November 11th since expected entry is “mmdd”, and this is valid date.

    • “666”: June 6th 2016 (could be a different year but ending in 6)

    • “6616”: June 6th 2016

    • “23 12”: 12th December since 23 could not be a month

  • Day of the Week (i.e., “Tue”): this will return the date that matches the day of the week. The Day of the Week would be day of the week name or a supported abbreviation (defined in the XML file).
  • Relative (i.e., “-12”): this will return the date the number of days from the reference date. To identify it is a relative, need to precede a number with a sign.
  • Special (i.e., “Memorial”): This will return the date that matches a string included in the XML file, normally a holiday. A special date can currently be set with a week of the month, month, a day of the week, or a month and day.

The Process

There are 3 basic processes:

No Relative Entry: Means that there is no numeric adjustment to the date after other processing. A numeric adjustment is indicated by the user with a sign in front of a number.

Relative without Day of Week Adjustment: If an entry includes a “relative” entry, it is treated as something that is done after the other processing. Thus the processing would be identical with and without the relative entry and then the result would be adjusted if necessary for the relative entry. This is normally days, so you would include “+8” to adjust the returned date with one 8 days in the future.

Day of the Week and Relative: The third path handles entries that include Day of the Week and relative. This is because the processing for relative changes there is a Day of the Week entry.  It is weeks instead of days, and a plus entry will be the future with “+1” being the next day in the future that is that day of the week, and each increase in the relative will be another week in the future, with the negative numbers being the same except the past. A “+0” or “-0” will return the closest day that is that day of the week. Thus Day of the Week would return the same value whether the using a preference for the past or future… This processing also has to take into effect that some user entries could be assumed to be a Day of the Week or a Month, depending on other user input.

public bool GetDate() { if (Tokens.ContainsKey(EnumKeywordType.relative)) { if (Tokens.ContainsKey(EnumKeywordType.dayOfWeek)) { var availableTokens = Tokens.Where(i => i.Key != EnumKeywordType.relative && i.Key != EnumKeywordType.dayOfWeek).ToList(); Date = TokenCountSwitch(availableTokens, DateAdjuster); Date = Utilities.DateForDayOfWeekRelative(Date, Tokens.Value(EnumKeywordType.dayOfWeek), Tokens.Value(EnumKeywordType.relative).ToInt()); } else if (Tokens.ContainsKey(EnumKeywordType.multiple) && (new ProcessSpecial(Tokens.Value(EnumKeywordType.multiple)).DayOfWeek != null)) { var availableTokens = Tokens.Where(i => i.Key != EnumKeywordType.relative && i.Key != EnumKeywordType.multiple).ToList(); Date = TokenCountSwitch(availableTokens, DateAdjuster); Date = Utilities.DateForDayOfWeekRelative(Date, new ProcessSpecial(Tokens.Value(EnumKeywordType.multiple)) .DayOfWeek.ToString(), Tokens.Value(EnumKeywordType.relative).ToInt()); } else { var availableTokens = Tokens.Where(i => i.Key != EnumKeywordType.relative).ToList(); Date = TokenCountSwitch(availableTokens, DateAdjuster); Date = Date.AdjustDays(Tokens.Value(EnumKeywordType.relative).ToInt()); } } else { Date = TokenCountSwitch(Tokens, DateAdjuster); } Tokens.Clear(); return IsValid; } private static Date TokenCountSwitch(List<KeyValuePair<EnumKeywordType, string>> Tokens, abstractDateResolve DateAdjuster) { switch (Tokens.Count) { case 0: return DateAdjuster.RelativeDate; case 1: return OneToken(Tokens.First(), DateAdjuster); case 2: return TwoToken(Tokens, DateAdjuster); case 3: return ThreeToken(Tokens, DateAdjuster); } throw new ArgumentException("too many tokens"); }

Double Entry:

Number and Month (i.e., “Nov 12”): The month would match the month name or a supported abbreviation (defined in the XML file). The number can be of up to 6 digits or two separate sets contiguous digits.

Relative Day of the Week (i.e., “+4 fr”): This will return a date that corresponds to the day of the week the number of weeks specified by the relative value (a numeric value with a sign).

Ordinal Day of the Week (i.e., “First Friday”): Will return the day of the week that is in the specified week of the month, either current month or adjacent depending on the resolution selected (past, future …).

Triple Entry:

Ordinal Day of the Week with Month (i.e., “First Friday Nov”): Will return the day of the week that is in the specified month, week of the month, either current year or adjacent depending on the resolution selected (past, future …).

Date Class

All caluclations are returned in the Date class. This class is used in part because it included methods that are used during the calulcaton of the date, but more importantly is has an ErrorMessage property that is null unless there was an issue with processing the input. The ToString method will return this string if it is non-null or the date using the current culture since it uses the DataTime ToShortDateString method.

Using the Code

To use the code need to initialize an instance of the Date Processor:

var dateProcessor = new DateProcessor(DateTime.Now, "QuickDateStrings.xml", EnumDateResolve.RelativePast, EnumOrderPreference.MDY);

Currently have to provide the date for which the input date will be relative, a path to the XML file that contains the keywords, and enumerations that will indicate the date resolution preference, and the order of entry preference. This code is still in pretty raw form, and eventually would probably want the XML file to be determined by the current culture, and the relative date to default to the current date. I do not default the date because I have a test file, and need to have a defined relative date so that the results of processing can be compared to a predetermined expected value.

To use instance, just need to call the Parse method with the user input:

var dt = dateProcessor.Parse(input);

Currently the result is the Date class defined in the QuickDateEntryLibrary project. There is a ToDateTime method defined in this class to convert to a DateTime.

Sample

The Sample has serveral projects, with several ways to test/use the Quick Date Library. The projects currently only rationalizes dates to the past, is set up only for the MDY order preference, and uses an XML file that contains American strings (for holidays, month names, day of the week names, etc.).

The Quick Date Library Project (QuickDateEntryLibrary)

This is the part that should be included in your solution to use this functionality.

The Console Project (QuickDataEntryConsoleTest)

This is a console application that has allows the input of different strings, and the results will be displayed, including any error messages. The WPF application does not provide the same level of detailed informtion.

The WPF Project (QuickDateWpf)

This is a project that shows how the Quick Date can be used in a WPF application. It is a very simple implementation that uses an IValueConverter. The problem is that there is no way to provide error feedback to the user since the converter only converts for the ViewModel, and then any error information is lost. Also, had to have another control that could take focus (used the Result TextBox) since the update is only after the control with the date entry loses focus. I am investigating a better implemention, and open to suggestions.

The Unit Test (QuickDateUnitTest)

The unit test uses a CSV file  (“TestQuickDate.csv”) to provide values to test. The results are currently output into another CSV file (“TestResults.csv”). It will then open the results CSV file in Excel. Eventually would like to go through and determine the correct values for all input strings and relative dates in the CSV, and then can have a much improved version.

Enter the text “fr 3rd”

On 06/08/2017 the result would be May 19, 2017

Conclusion

This article has been a long time coming, years. Part of the reason is that I really had no good experience in parcing, and was not sure I liked the design I came up with, and it has evolved over time. Definately some big improvements, and this time, when I went through and only did some minor tweeks to core code, I decided it must be acceptable, and I did want to get the concept out to the community.

I have personally used the day of the week and the strictly number input quite a bit (this was all that I initially implemented), and I really like it. It requires a little thought when using this form of input because you have to think about how the input could be confused (i.e., “31” could result in the 31st of the month or “March  1st”, whereas 32 will always result in “March 2nd”), but I have been very happy the improvement. I have since added quite a few other enhancements because I thought they might be useful to other users, and to show that design will support a lot of flexibility.

I would not say that this is a replacement for the DatePicker, but more of a supliment. The DatePicker does provide a good way to look at and select a date, but it is not perfect.

There are certainly quite a few other ideas that may be worth adding to this concept. Hopefully the design is flexible enough to handle them. Please provide feedback.

History

Initial Verson.

LEAVE A REPLY