Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Map enum to a function/action using enum-specific method

I'm working on an online booking site (airline) and I want to validate if the user/customer's chosen route is valid according to some settings. The existing codes uses a lot of enums and I found myself doing lots of if/if else/else to map a specific enum to a particular action that I wanted to happen. What I want to do is to write a enum-specific method that would do the mapping for me. Is there any standard way to do this?

Here's a simplified version of the app code using the same class names/enum values etc from the real app:

// real app has 9 members, shortened for simplicity's sake
public enum RegionType
{
    Station,
    Country,
    All
}

public enum Directionality
{
    Between,
    From,
    To
}

// simplified version
public class Flight
{
     public RegionType RegionType { get; set; }
     public RegionType TravelRegionType { get; set; }
     public string RegionCode { get; set; }
     public string TravelRegionCode { get; set; }
     public string RegionCountryCode { get; set; }
     public string TravelRegionCountryCode { get; set; }
     public Directionality Directionality { get; set; }
}

Here's some sample usage:

    // valid flight
    Flight flight = new Flight()
    {
        RegionCode = "NY",
        CountryCode = "JP",
        RegionType = RegionType.Station,
        TravelRegionType = RegionType.Country,
        Directionality = Directionality.Between
    };

    // these are the station code/country code that user selected
    // needs to be validated against the Flight object above
    var userSelectedRoutes = new List<KeyValuePair<string, string>>() 
    { 
        new KeyValuePair<string, string>("NY", "JP"),
        new KeyValuePair<string, string>("NY", "AU"),
        new KeyValuePair<string, string>("JP", "NY")
    };

Some code validation I wrote to lessen nested if/else if/else enum matching:

private bool IsRouteValid(Directionality direction, string origin, 
                          string destination, string departure, string arrival)
{
    // both departure station and arrival station
    if (direction == Directionality.Between)
    {
        return (origin.Equals(departure, StringComparison.OrdinalIgnoreCase) 
          && destination.Equals(arrival, StringComparison.OrdinalIgnoreCase)
               || origin.Equals(arrival, StringComparison.OrdinalIgnoreCase) 
          && destination.Equals(departure, StringComparison.OrdinalIgnoreCase));
    }
    else if (direction == Directionality.From)
    {
            return (origin.Equals(departure, 
                    StringComparison.OrdinalIgnoreCase));   
    }
    else if (direction == Directionality.To)
    {
            return (destination.Equals(arrival, 
                    StringComparison.OrdinalIgnoreCase));
    }

    return false;
}

And here's the messy code I want to change:

if (flight.RegionType == RegionType.Station 
    && flight.TravelRegionType == RegionType.Country)
{
     return userSelectedRoutes.Any(route => 
            IsRouteValid(flight.Directionality, route.Key, route.Value,
            flight.RegionCode, flight.TravelRegionCode));
}
else if (flight.RegionType == RegionType.Country 
&& flight.TravelRegionType == RegionType.Station)
{
    return userSelectedRoutes.Any(route => 
           IsRouteValid(flight.Directionality, route.Key, route.Value,
           flight.CountryCode, flight.RegionCode));
}
else if (flight.RegionType == RegionType.Station 
&& flight.TravelRegionType == RegionType.Station)
{
    return userSelectedRoutes.Any(route => 
           IsRouteValid(flight.Directionality, route.Key, route.Value,
                       flight.RegionCode, flight.TravelRegionCode));
}
else if (flight.RegionType == RegionType.Station 
&& flight.TravelRegionType == RegionType.All)
{
    return userSelectedRoutes.Any(route => 
           IsRouteValid(flight.Directionality, route.Key, route.Value,
                       flight.RegionCode, route.Value));
}
else if (flight.RegionType == RegionType.All 
&& flight.TravelRegionType == RegionType.Station)
{
    return userSelectedRoutes.Any(route =>
           IsRouteValid(flight.Directionality, route.Key, route.Value,
           route.Key, flight.TravelRegionCode));
}
else if (flight.RegionType == RegionType.All 
        && flight.TravelRegionType == RegionType.All)
{
    return true;
}
else
{
    return false;
} 

Legend:

RegionCode = departure station/origin
TravelRegionCode = arrival station/destination
Between = routes must be only from the given departure station and arrival station and vice-versa (ex NY-JP or JP-NY)
From = from particular station to any routes (ex AU-All)
To = any routes to a particular station (ex All-AU)

If you could notice, the .Any of all the conditions above are same with slight changes. I want to reduce code redundancy, if possible. I used KeyValuePair so I have both departure station and arrival station on a single data type.

Any ideas on how could I make this code less messy/beautiful? I know I also hard coded IsRouteValid() but I'm 100% sure that Directionality could only have the 3 possible combinations. RegionType on the other hand could have several several combinations like Station-Station, Station-Country, Country-Station, Country-Country etc.

Expected Output:

Valid/True for first route (NY-JP)
Invalid/False for second route (NY-AU)
Valid/True for third route (JP-NY) [since Directionality is Between]

Thank you for reading this very long query and thanks in advance for your feedback and suggestions.

Similar post:

Enum and Dictionary

like image 338
Annie Lagang Avatar asked Jan 18 '13 08:01

Annie Lagang


2 Answers

A way to handle such enum-Action-Mappings is using dictionaries. Here's an example:

public enum MyEnum
{
    EnumValue1,
    EnumValue2,
    EnumValue3
}

private IDictionary<MyEnum, Action> Mapping = new Dictionary<MyEnum, Action>
    {
        { MyEnum.EnumValue1, () => { /* Action 1 */ } },
        { MyEnum.EnumValue2, () => { /* Action 2 */ } },
        { MyEnum.EnumValue3, () => { /* Action 3 */ } }
    };

public void HandleEnumValue(MyEnum enumValue)
{
    if (Mapping.ContainsKey(enumValue))
    {
        Mapping[enumValue]();
    }
}

Of course you can also use Func instead of Action to handle parameters.

Edit:

As you're not only using one enumeration but pairs of enumerations you would have to adjust the example above to maybe handle a Tuple or another way to aggregate the enum values.

like image 153
MatthiasG Avatar answered Nov 07 '22 09:11

MatthiasG


Following @MatthiasG suggestion, here's the code I'd ended up writing:

private List<KeyValuePair<RegionType, string>> 
                GetRegionTypeAndValueMapping(Flight flight, 
                                             RegionType regionType,
                                             RegionType travelRegionType)
{
    var mapping = new List<KeyValuePair<RegionType, string>>();
    if(regionType == RegionType.Station)
    {
        mapping.Add(new KeyValuePair<RegionType, string>
                          (RegionType.Station, flight.RegionCode));
    }
    else if(regionType == RegionType.Country)
    {
        mapping.Add(new KeyValuePair<RegionType, string>
                          (RegionType.Country, flight.RegionCountryCode));
    }
    else if(regionType == RegionType.All)
    {
        mapping.Add(new KeyValuePair<RegionType, string>
                          (RegionType.All, null));
    }

    if(travelRegionType == RegionType.Station)
    {
        mapping.Add(new KeyValuePair<RegionType, string>
                          (RegionType.Station, flight.TravelRegionCode));
    }
    else if(travelRegionType == RegionType.Country)
    {
        mapping.Add(new KeyValuePair<RegionType, string>
                          (RegionType.Country, 
                           flight.TravelRegionCountryCode));
    }
    else if(travelRegionType == RegionType.All)
    {
        mapping.Add(new KeyValuePair<RegionType, string>
                          (RegionType.All, null));
    }

    return mapping;
}

I used List<KeyValuePair<RegionType, string>> because Dictionary doesn't allow key duplicates. My keys are of RegionType enum and there will be a time that both departure and arrival stations will have the same RegionType enum. (i.e. Station-Station, Country-Country and All-All).

// Copyright (c) 2010 Alex Regueiro
// Licensed under MIT license, available at 
// <http://www.opensource.org/licenses/mit-license.php>.
// Published originally at 
// <http://blog.noldorin.com/2010/05/combinatorics-in-csharp/>.
// Version 1.0, released 22nd May 2010.

// modified by moi to be a generator 
public static IEnumerable<T[]> GetPermutations<T>(IList<T> list, 
                                                  int? resultSize, 
                                                  bool withRepetition)
{
    if (list == null)
    {
        throw new ArgumentNullException("Source list is null.");
    }

    if (resultSize.HasValue && resultSize.Value <= 0)
    {
        throw new ArgumentException("Result size must be any 
                                     number greater than zero.");
    }

    var result = new T[resultSize.HasValue ? resultSize.Value : list.Count];
    var indices = new int[result.Length];
    for (int i = 0; i < indices.Length; i++)
    {
        indices[i] = withRepetition ? -1 : i - 1;
    }

    int curIndex = 0;
    while (curIndex != -1)
    {
        indices[curIndex]++;
        if (indices[curIndex] == list.Count)
        {
            indices[curIndex] = withRepetition ? -1 : curIndex - 1;
            curIndex--;
        }
        else
        {
            result[curIndex] = list[indices[curIndex]];
            if (curIndex < indices.Length - 1)
            {
                curIndex++;
            }
            else
            {
                yield return result;
            }

        }
    }
}

Okay, I cheated. :P I needed a method that will compute permutations with repetitions, so instead of writing one, I googled. (hehe) The reason why I used permutations is to avoid hard coding all the possible combinations that RegionType might have. I modified Alex Regueiro's method to be a generator so that I could use Linq for it. For a refresher on Permutation and Combination, see this very excellent math stackexchange post.

I modified IsRouteValid() to handle RegionType.All whose value is null.

Here's the modified version:

private bool IsRouteValid(Directionality direction, string origin, 
                          string destination, string departure, 
                          string arrival)
{
     // ** == All stations/countries
     if ((origin == null && departure == "**") &&
         (destination == null && arrival == "**"))
     {
         return true;
     }
     else if (origin == null && departure == "**")
     {
        return destination.Equals(arrival, StringComparison.OrdinalIgnoreCase);
     }
     else if (destination == null && arrival == "**")
     { 
       return origin.Equals(departure, StringComparison.OrdinalIgnoreCase);
     }

    // both departure station and arrival station
    if (direction == Directionality.Between)
    {
            return (origin.Equals(departure,
                    StringComparison.OrdinalIgnoreCase) && 
                    destination.Equals(arrival, 
                    StringComparison.OrdinalIgnoreCase) || 
                    origin.Equals(arrival, 
                    StringComparison.OrdinalIgnoreCase) &&         
                    destination.Equals(departure,
                    StringComparison.OrdinalIgnoreCase));
    }
    else if (direction == Directionality.From)
    {
        return (origin.Equals(arrival, StringComparison.OrdinalIgnoreCase));
    }
    else if (direction == Directionality.To)
    {
        return (destination.Equals(departure, 
               StringComparison.OrdinalIgnoreCase));
    }

    return false;
}

It's show time!

RegionType[] allRegionTypes = (RegionType[]) 
                              Enum.GetValues(typeof(RegionType));

var validFlights = 
      GetPermutations<RegionType>(allRegionTypes, 2, true)
         .Select(perm => new 
                         { 
                           RegionType = perm.First(),
                           TravelRegionType = perm.Last() 
                         })
         .Where(result => result.RegionType == flight.RegionType &&  
                          result.TravelRegionType ==                            
                          flight.TravelRegionType)
         .Select(map => 
                   GetRegionTypeAndValueMapping(flight,
                     map.RegionType, 
                     map.TravelRegionType));

    // same functionality as my previous messy code
    // validates all flights selected by user
    // it doesn't matter if not all flights are valid
    // as long as one of them is
    foreach(var validFlight in validFlights)
    {
        userSelectedRoutes.Any(kvp => IsRouteValid(flight.Directionality, 
                                                   kvp.Key, 
                                                   kvp.Value, 
                                                   validFlight.First().Value,
                                                   validFlight.Last().Value))
                                      .Dump("Any Flight");
    }

I created this code to demonstrate how I got the result which is the same as the my expected results above.

    foreach(var route in userSelectedRoutes)
    {
        foreach(var validFlight in validFlights)
        {
            bool condition = IsRouteValid(flight.Directionality, 
                                          route.Key, 
                                          route.Value, 
                                          validFlight.First().Value, 
                                          validFlight.Last().Value);

            Console.WriteLine(string.Format("{0}-{1} {2}", 
                                               route.Key,     
                                               route.Value, 
                                               condition.ToString()));
        }
    }

Results:

expected results screenshot

Note:

.Dump() is a Linqpad extension.

like image 42
Annie Lagang Avatar answered Nov 07 '22 10:11

Annie Lagang