Enumeration and Matching Strings

I am trying to read an XML file. One of the values ​​has a suffix, for example. " 30d ". It means 30 days. So I am trying to convert this to DateTime.Now.AddDays(30) . To read this field in XML, I decided to use Enum:

 enum DurationType { Min = "m", Hours = "h", Days = "d" } 

Now I’m not quite sure how to approach this efficiently (I’m a little stupid when it comes to transfers). Should I separate the suffix, in this case "d", from the string first, and then try and match it in enum with the switch ?

I think if you omit my question, it will be: What is the best way to get from 30d to DateTime.Now.AddDays(30) ?

+6
source share
8 answers

You can do ExtensionMethod to parse the string and return the DateTime you want

Sort of:

  public static DateTime AddDuration(this DateTime datetime, string str) { int value = 0; int mutiplier = str.EndsWith("d") ? 1440 : str.EndsWith("h") ? 60 : 1; if (int.TryParse(str.TrimEnd(new char[]{'m','h','d'}), out value)) { return datetime.AddMinutes(value * mutiplier); } return datetime; } 

Using:

  var date = DateTime.Now.AddDuration("2d"); 
+6
source

This seems like a good place to use regular expressions; in particular, capture groups.

The following is a working example:

 using System; using System.Text.RegularExpressions; namespace RegexCaptureGroups { class Program { // Below is a breakdown of this regular expression: // First, one or more digits followed by "d" or "D" to represent days. // Second, one or more digits followed by "h" or "H" to represent hours. // Third, one or more digits followed by "m" or "M" to represent minutes. // Each component can be separated by any number of spaces, or none. private static readonly Regex DurationRegex = new Regex(@"((?<Days>\d+)d)?\s*((?<Hours>\d+)h)?\s*((?<Minutes>\d+)m)?", RegexOptions.IgnoreCase); public static TimeSpan ParseDuration(string input) { var match = DurationRegex.Match(input); var days = match.Groups["Days"].Value; var hours = match.Groups["Hours"].Value; var minutes = match.Groups["Minutes"].Value; int daysAsInt32, hoursAsInt32, minutesAsInt32; if (!int.TryParse(days, out daysAsInt32)) daysAsInt32 = 0; if (!int.TryParse(hours, out hoursAsInt32)) hoursAsInt32 = 0; if (!int.TryParse(minutes, out minutesAsInt32)) minutesAsInt32 = 0; return new TimeSpan(daysAsInt32, hoursAsInt32, minutesAsInt32, 0); } static void Main(string[] args) { Console.WriteLine(ParseDuration("30d")); Console.WriteLine(ParseDuration("12h")); Console.WriteLine(ParseDuration("20m")); Console.WriteLine(ParseDuration("1d 12h")); Console.WriteLine(ParseDuration("5d 30m")); Console.WriteLine(ParseDuration("1d 12h 20m")); Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } } 

EDIT: Below is an alternative, a slightly more succinct version above, although I'm not sure which one I prefer more. I'm usually not a fan of code that is too tight. I adjusted the regular expression to put a limit of 10 digits on each number. This allows me to use the int.Parse function int.Parse , because I know that the input consists of at least one digit and no more than ten (unless it is fixed at all, in which case it will be an empty string: therefore, the purpose of the ParseInt32ZeroIfNullOrEmpty method )

  // Below is a breakdown of this regular expression: // First, one to ten digits followed by "d" or "D" to represent days. // Second, one to ten digits followed by "h" or "H" to represent hours. // Third, one to ten digits followed by "m" or "M" to represent minutes. // Each component can be separated by any number of spaces, or none. private static readonly Regex DurationRegex = new Regex(@"((?<Days>\d{1,10})d)?\s*((?<Hours>\d{1,10})h)?\s*((?<Minutes>\d{1,10})m)?", RegexOptions.IgnoreCase); private static int ParseInt32ZeroIfNullOrEmpty(string input) { return string.IsNullOrEmpty(input) ? 0 : int.Parse(input); } public static TimeSpan ParseDuration(string input) { var match = DurationRegex.Match(input); return new TimeSpan( ParseInt32ZeroIfNullOrEmpty(match.Groups["Days"].Value), ParseInt32ZeroIfNullOrEmpty(match.Groups["Hours"].Value), ParseInt32ZeroIfNullOrEmpty(match.Groups["Minutes"].Value), 0); } 

EDIT: just to take this one more step, I added another version that handles days, hours, minutes, seconds and milliseconds, with different abbreviations for each. I split the regex into several lines for readability. Note that I also had to adjust the expression using (\b|(?=[^az])) at the end of each component: this is because the "ms" block was captured as the "m" unit. The special syntax "? =" Used with "[az]" indicates a match for the character, but does not "consume" it.

  // Below is a breakdown of this regular expression: // First, one to ten digits followed by "d", "dy", "dys", "day", or "days". // Second, one to ten digits followed by "h", "hr", "hrs", "hour", or "hours". // Third, one to ten digits followed by "m", "min", "minute", or "minutes". // Fourth, one to ten digits followed by "s", "sec", "second", or "seconds". // Fifth, one to ten digits followed by "ms", "msec", "millisec", "millisecond", or "milliseconds". // Each component may be separated by any number of spaces, or none. // The expression is case-insensitive. private static readonly Regex DurationRegex = new Regex(@" ((?<Days>\d{1,10})(d|dy|dys|day|days)(\b|(?=[^az])))?\s* ((?<Hours>\d{1,10})(h|hr|hrs|hour|hours)(\b|(?=[^az])))?\s* ((?<Minutes>\d{1,10})(m|min|minute|minutes)(\b|(?=[^az])))?\s* ((?<Seconds>\d{1,10})(s|sec|second|seconds)(\b|(?=[^az])))?\s* ((?<Milliseconds>\d{1,10})(ms|msec|millisec|millisecond|milliseconds)(\b|(?=[^az])))?", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); private static int ParseInt32ZeroIfNullOrEmpty(string input) { return string.IsNullOrEmpty(input) ? 0 : int.Parse(input); } public static TimeSpan ParseDuration(string input) { var match = DurationRegex.Match(input); return new TimeSpan( ParseInt32ZeroIfNullOrEmpty(match.Groups["Days"].Value), ParseInt32ZeroIfNullOrEmpty(match.Groups["Hours"].Value), ParseInt32ZeroIfNullOrEmpty(match.Groups["Minutes"].Value), ParseInt32ZeroIfNullOrEmpty(match.Groups["Seconds"].Value), ParseInt32ZeroIfNullOrEmpty(match.Groups["Milliseconds"].Value)); } 
+5
source

update: Do not vote for it. I leave this simply because it is an alternative approach. Instead, look at sa_ddam213 and Dr. Wiley's Apprentice answer.

Should I separate the suffix, in this case "d", from the string first try and match it in the enum using the switch statement?

Yes.

For a fully functional example:

 private void button1_Click( object sender, EventArgs e ) { String value = "30d"; Duration d = (Duration)Enum.Parse(typeof(Duration), value.Substring(value.Length - 1, 1).ToUpper()); DateTime result = d.From(new DateTime(), value); MessageBox.Show(result.ToString()); } enum Duration { D, W, M, Y }; static class DurationExtensions { public static DateTime From( this Duration duration, DateTime dateTime, Int32 period ) { switch (duration) { case Duration.D: return dateTime.AddDays(period); case Duration.W: return dateTime.AddDays((period*7)); case Duration.M: return dateTime.AddMonths(period); case Duration.Y: return dateTime.AddYears(period); default: throw new ArgumentOutOfRangeException("duration"); } } public static DateTime From( this Duration duration, DateTime dateTime, String fullValue ) { Int32 period = Convert.ToInt32(fullValue.ToUpper().Replace(duration.ToString(), String.Empty)); return From(duration, dateTime, period); } } 
+3
source

I really don't understand how enum helps here.

This is how I could approach him.

 string s = "30d"; int typeIndex = s.IndexOfAny(new char[] { 'd', 'w', 'm' }); if (typeIndex > 0) { int value = int.Parse(s.Substring(0, typeIndex)); switch (s[typeIndex]) { case 'd': result = DateTime.Now.AddDays(value); break; case 'w': result = DateTime.Now.AddDays(value * 7); break; case 'm': result = DateTime.Now.AddMonths(value); break; } } 

Depending on the reliability of your input, you may need int.TryParse() instead of int.Parse() . Otherwise, that should be all you need.

Note. I also wrote a sscanf() replacement for .NET that would handle this pretty easily. You can see the code for this in the article Replacing sscanf () for .NET .

+2
source

Try using the following code, assuming that the values ​​of type "30d" are in the string "val".

 DateTime ConvertValue(string val) { if (val.Length > 0) { int prefix = Convert.ToInt32(val.Length.Remove(val.Length-1)); switch (val[val.Length-1]) { case 'd': return DateTime.Now.AddDays(prefix); case 'm': return DateTime.Now.AddMonths(prefix); // etc. } throw new ArgumentException("string in unexpected format."); } 
+1
source

Example console application / tutorial example:

 enum DurationType { [DisplayName("m")] Min = 1, [DisplayName("h")] Hours = 1 * 60, [DisplayName("d")] Days = 1 * 60 * 24 } internal class Program { private static void Main(string[] args) { string input1 = "10h"; string input2 = "1d10h3m"; var x = GetOffsetFromDate(DateTime.Now, input1); var y = GetOffsetFromDate(DateTime.Now, input2); } private static Dictionary<string, DurationType> suffixDictionary { get { return Enum .GetValues(typeof (DurationType)) .Cast<DurationType>() .ToDictionary(duration => duration.GetDisplayName(), duration => duration); } } public static DateTime GetOffsetFromDate(DateTime date, string input) { MatchCollection matches = Regex.Matches(input, @"(\d+)([a-zA-Z]+)"); foreach (Match match in matches) { int numberPart = Int32.Parse(match.Groups[1].Value); string suffix = match.Groups[2].Value; date = date.AddMinutes((int)suffixDictionary[suffix]); } return date; } } [AttributeUsage(AttributeTargets.Field)] public class DisplayNameAttribute : Attribute { public DisplayNameAttribute(String name) { this.name = name; } protected String name; public String Name { get { return this.name; } } } public static class ExtensionClass { public static string GetDisplayName<TValue>(this TValue value) where TValue : struct, IConvertible { FieldInfo fi = typeof(TValue).GetField(value.ToString()); DisplayNameAttribute attribute = (DisplayNameAttribute)fi.GetCustomAttributes(typeof(DisplayNameAttribute), false).FirstOrDefault(); if (attribute != null) return attribute.Name; return value.ToString(); } } 

Uses an attribute to determine your suffix, uses an enum value to determine your offset.

It is required:

 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; 

It can be considered a hack to use the integer value enum, but this example will still allow you to parse all the enumerations (for any other use, such as a switch case), with a few tricks.

+1
source

Reserves cannot be copied using non-numeric types, so there are no row-based enumerations. Perhaps you can overdo it. Without knowing more about the problem, the simplest solution seems to separate the last character, converting the rest to int and then treating each final char as a separate case.

0
source

I would suggest using regexp to divide the number first and execute the Enum.Parse Method to evaluate the value of the enumeration. How can you use the switch (see Corylulu's answer) to get the correct offset based on the number being analyzed and the value of the enumeration.

0
source

All Articles