2

I have a c# class that contains 12 boolean properties;

AvaialableJan, AvailableFeb, AvailableMar, AvailableApr... etc

Each instance of this class can have any number of them as true, and normally they run in a sequence. I.e. AvailableFeb - AvailableApr will be true but then none others. Sometimes there will be only a single bool as true, i.e. only available in one month. Sometimes they all will be, i.e. available year round.

Sometimes, and this is the tricky bit, they will have two ranges, i.e. available Feb-Apr, and Aug-Oct.

I am trying to write a function to return a string to represent the selections made.

Only 1 bool selected I would like to return (e.g.): "Feb only".

Range selected (e.g.): "Jan-Mar".

Multiple ranges (e.g.): "Jan-Mar, Aug-Nov".

Mixed single and range (e.g.): "Jan-Mar, Sep".

All selected: "Year round".

A range is where consecutive months have been assigned true, e.g. Jan, Feb, Mar being true should result in Jan-Mar.

I have tried using a loop with simple condition checks but it's just messy and I can't get it right, I regret storing them as individual properties rather than an int array but I am stuck with this now. I wonder if there is a way to store the bools inside another property using reflection then looping. Haven't had luck so far. Any help much appreciated!

Thanks

Grenville
  • 235
  • 2
  • 13

3 Answers3

2

If you care to change your class a bit then follow this way!.

You can have 12 properties and map them into single int array. So you have int array of length 12. when intArray[0] = 1 means first month is available. if intArray[0] = 0 means first month is not available and so on.

You convert this int array into string with base-16 representation of 1-based index of each month, but if its not available then put 0 instead. Later we use a dictionary to get the name of months by giving its number instead.

Example 1:

"003456000a00"

This means months 3th,4th,5th,6th and a = 10th are available. then use regex to parse this and find the matches. The regex pattern would be [^0]+. means it matches any character except 0.

So the regex will give us this matches.

Match 1 : 3456
Match 2 : a

First match length is more than 1 means its a range of months. so we take the first character and last one. and we join them with -. Here it will be 3 and 6. So match 1 should become

Mar-Jun

Second match length is only 1 means its a single month. So it should become

Oct

Since we have two matches we join these with , and finally the out put is Mar-Jun , Oct

Example 2:

"020000000000"

Matches

Match 1 : 2

Since we have only 1 match and the match length is 1 this should become

Feb Only

Example 3:

"023456780000"

Matches

Match 1 : 234567

This is only one match but the length of this match is more than one. so we just take 2 and 7 and join them with -.

Feb-Jul

Example 4:

"123456789abc"

Matches

Match 1 : 123456789abc

As you can see here we have all the months. the length of this match is 12 so it should be

Year round

Example 5:

"123456000abc"

Matches

Match 1 : 123456
Match 2 : abc

Here we have two matches. it can be Jan-Jun , Oct-Dec but the better representation (as you mentioned in comment) is Oct-Jun. That should be abc123456. So we check if the last match ends with c and first match starts with 1 then we join last match with first match.

Oct-Jun

Code:

It will become simple as you can see.

internal class AvailableYear
{
    private readonly int[] _available;
    private static readonly Regex MatchTrue = new Regex("[^0]+");
    private static readonly Dictionary<string, string> GetName = new Dictionary<string, string>
    {
        {"1","Jan" },
        {"2","Feb" },
        {"3","Mar" },
        {"4","Apr" },
        {"5","May" },
        {"6","Jun" },
        {"7","Jul" },
        {"8","Aug" },
        {"9","Sep" },
        {"a","Oct" },
        {"b","Nov" },
        {"c","Dec" },
    };

    public AvailableYear(params int[] available)
    {
        if (available.Length > 12) throw new IndexOutOfRangeException("given parameters should not exceed 12 months.");

        _available = available;
    }

    public int AvaialableJan
    {
        get { return _available[0]; }
        set { _available[0] = value; }
    }

    public int AvailableFeb
    {
        get { return _available[1]; }
        set { _available[1] = value; }
    }
    public int AvailableMar
    {
        get { return _available[2]; }
        set { _available[2] = value; }
    }
    public int AvailableApr
    {
        get { return _available[3]; }
        set { _available[3] = value; }
    }
    public int AvaialableMay
    {
        get { return _available[4]; }
        set { _available[4] = value; }
    }
    public int AvaialableJun
    {
        get { return _available[5]; }
        set { _available[5] = value; }
    }
    public int AvaialableJul
    {
        get { return _available[6]; }
        set { _available[6] = value; }
    }
    public int AvaialableAug
    {
        get { return _available[7]; }
        set { _available[7] = value; }
    }
    public int AvaialableSep
    {
        get { return _available[8]; }
        set { _available[8] = value; }
    }
    public int AvaialableOct
    {
        get { return _available[9]; }
        set { _available[9] = value; }
    }
    public int AvaialableNov
    {
        get { return _available[10]; }
        set { _available[10] = value; }
    }
    public int AvaialableDec
    {
        get { return _available[11]; }
        set { _available[11] = value; }
    }

    public override string ToString()
    {
        string values = string.Join("", _available.Select((x, i) => x == 0 ? "0" : Convert.ToString(i + 1, 16)));
        var matches = MatchTrue.Matches(values).Cast<Match>().Select(x => x.Value).ToList();

        if (matches.Count == 0)
        {
            return "None";
        }
        if (matches[0].Length == 12)
        {
            return "Year round";
        }
        if (matches.Count == 1 && matches[0].Length == 1)
        {
            return GetName[matches[0]] + " Only";
        }
        else
        {
            if (matches.First().StartsWith("1") && matches.Last().EndsWith("c"))
            {
                matches[0] = matches.Last() + matches.First();
                matches.RemoveAt(matches.Count - 1);
            }

            List<string> output = new List<string>();

            foreach (var match in matches)
            {
                if (match.Length == 1)
                {
                    output.Add(GetName[match]);
                }
                else
                {
                    output.Add(GetName[match.First().ToString()] + "-" +
                               GetName[match.Last().ToString()]);
                }
            }

            return string.Join(", ", output);
        }
    }
}

Here is the test.

static void Main()
{
    AvailableYear ay = new AvailableYear(1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0);
    Console.WriteLine(ay.ToString());

    // Output : Jan , Apr-Jul , Nov
}

Update :

If you want to assign bool values in constructor you can change constructor to this.

public AvailableYear(params bool[] available)
{
    if (available.Length > 12) throw new IndexOutOfRangeException("given parameters should not exceed 12 months.");

    _available = available.Select(Convert.ToInt32).ToArray();
}

And the create the instance like this. without writing Convert.ToInt32 each time.

return new AvailableYear(AvailableJan, AvailableFeb, AvailableMar...., AvailableDec).ToString();
M.kazem Akhgary
  • 18,645
  • 8
  • 57
  • 118
  • 1
    Great answer! Thanks for taking the time to write that - I never thought of using RE. The very only issue with this solution is that it always starts in January, but say for example, I had Oct, Nov, Dec, Jan, Feb as true, then it should be Oct-Feb. The above solution gives Jan-Feb, Oct-Dec. I will try and figure out how to change that. – Grenville Nov 15 '15 at 17:47
  • @Grenville add another condition to fix that problem, see example 5 ;) – M.kazem Akhgary Nov 15 '15 at 19:01
  • That works now! I updated this to be the answer as it allows for flexible month names (i.e. if we wanted to say January rather than just Jan). Instead of changing my class so much I just created this as a separate class and then called it as so, return new AvailableYear(Convert.ToInt32(AvailableJan), Convert.ToInt32(AvailableFeb), Convert.ToInt32(AvailableMar)...., Convert.ToInt32(AvailableDec)).ToString(); – Grenville Nov 16 '15 at 09:01
  • @Grenville see the update. you can also change constructor to accept `bool[]` instead of `int[]` and then you can simplify what you have written. – M.kazem Akhgary Nov 16 '15 at 09:08
1

You can do it without reflection like this:

string FormatMonths(MyClass myObject)
{
    return FormatMonths(
        myObject.AvailableJan,
        myObject.AvailableFeb,
        myObject.AvailableMar,
        myObject.AvailableApr,
        myObject.AvailableMay,
        myObject.AvailableJun,
        myObject.AvailableJul,
        myObject.AvailableAug,
        myObject.AvailableSep,
        myObject.AvailableOct,
        myObject.AvailableNov,
        myObject.AvailableDec);
}


private static string[] months = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

string FormatMonthRange(int startMonth, int endMonth, StringBuilder sb)
{
    if (startMonth == 0 && endMonth == 11)
        sb.Append("Year round");
    else if (endMonth == 11 && sb[0] == 'J' && sb[1] == 'a')
    {
        // this deals with wrap around from December to January:
        if (sb.Length > 3 && sb[3] == '-')
            sb.Remove(0, 4);
        sb.Insert(0, months[startMonth] + "-");
    }
    else
    {
        if (sb.Length > 0)
            sb.Append(", ");
        sb.Append(months[startMonth]);
        if (startMonth != endMonth)
            sb.Append("-").Append(months[endMonth]);
    }
}

string FormatMonths(params bool[] monthBools)
{
    var sb = new StringBuilder();
    int rangeStart = -1;
    for (int i = 0; i < monthBools.Length; i++)
    {
        if (monthBools[i])
        {
            if (rangeStart < 0)
                rangeStart = i;
        }
        else
        {
            if (rangeStart >= 0)
            {
                FormatMonthRange(rangeStart, i - 1, sb);
            }
            rangeStart = -1;
        }
    }
    if (rangeStart >= 0)
        FormatMonthRange(rangeStart, monthBools.Length - 1, sb);
    if (sb.Length == 3)
        sb.Append(" only");
    return sb.ToString();
}
Bryce Wagner
  • 2,640
  • 1
  • 26
  • 43
  • Thanks man, that is a nice answer, I fixed a few bits of code and ran but it kept starting in Jan even if Jan wasn't included. – Grenville Nov 15 '15 at 17:45
  • @Grenville Oops, tiny bug, that's what I get for not compiling it. I fixed the bug, changing "rangeStart = 0" to "rangeStart = i". – Bryce Wagner Nov 15 '15 at 18:29
  • Cheers Bruce! That has solved that problem :) now if I select Dec, Jan, Feb, Mar as true. The string is Dec-Jan-Mar rather than Dec-Mar. I am trying to figure out why now. – Grenville Nov 15 '15 at 18:40
  • @Grenville It was because in the wraparound code I put sb[4] == '-' instead of sb[3] == '-'. Should work now. – Bryce Wagner Nov 15 '15 at 19:24
  • Works perfectly now! Thanks a bunch – Grenville Nov 15 '15 at 19:40
0

Because you can not simply iterate over members like this, you could create some Attribute to detect those boolean memebrs by using an order number in the attribute. For example

class Year
{
    [Month(0)]
    public bool AvaialableJan { // ... }

    [Month(1)]
    public bool AvaialableFeb { // ... }
}

Than you could get them via reflection in a list and search for sequences. Take a look at: Reflection - get attribute name and value on property

But a better solution would be to work without reflections and a kind of ranges:

public class Month
{
    public string Name { //... }
}

public class Range
{
    public List<Month> Months //...
}

public class Year
{
    public List<Range> Ranges //...
}

But both solutions should work.

Community
  • 1
  • 1
BendEg
  • 20,098
  • 17
  • 57
  • 131