MSBuild Task Generator: Part 11. IntRange explained and the exciting conclusion ...

Tomorrow … next week … read: the next time I post.

 

Specifying an integer range is one of the problems we need solve to have a reasonably robust task generator.  It’s one thing to say that a property is assignable from an int, but another to say that the value must be valid.  We want to ensure that by the time the task is executing we know the properties are valid.

 

So first an example input and output:

 

<?xml version="1.0" encoding="utf-8" ?>

<Tasks>

      <Task Class="BasicType" Namespace="GenTaskTests">

            <Property Name="Int32Test" Type="System.Int32" Range="1;5;10-20"/>

      </Task>

</Tasks>

 

Generates:

 

// Auto-generated Task: 3/23/2004 11:18:49 AM

namespace GenTaskTests

{

    using System;

    using Microsoft.Build.Framework;

    using Microsoft.Build.Utilities;

    using MBFTaskUtils;

   

   

    public abstract class BasicType

    {

       

        #region Int32Test

        private int m_Int32Test;

       

        public virtual int Int32Test

        {

            get

            {

                return this.m_Int32Test;

            }

            set

            {

           string stringRange = "1;5;10-20";

                MBFTaskUtils.IntRangeCollection intRanges = MBFTaskUtils.IntRangeCollection.Parse(stringRange);

                if (intRanges.Satisfies(value))

                {

                    this.m_Int32Test = value;

                    return;

                }

                throw new System.ArgumentOutOfRangeException("Unexpected value type setting field \"Int32Test\". Expected value in range: 1;5;10" +

                        "-20");

            }

        }

        #endregion

    }

}

 

 

We won’t cover the CodeDOM today, since we’ve covered everything we need to know in previous posts.  Instead we’ll focus on the utility class, IntRangeCollection.

 

An IntRangeCollection is, as expected, a collection of IntRange instances.  An IntRange is a numeric range from N to M, inclusive, where N <= M and both N and M are System.Int32.

 

An IntRange is expressed as a literal “30” or a range “30-40” (the integers 30 to 40, inclusive).

 

An IntRangeCollection then has a simple job:

 

1) Split the range string into it’s semi-colon deliminated parts

2) For each part create an IntRange

3) Allow a value to be tested against all parts

 

Pretty simple conceptually and even easier to implement:

 

public class IntRangeCollection : List<IntRange>

{

      static public IntRangeCollection Parse(string ranges)

      {

            IntRangeCollection coll = new IntRangeCollection();

            string[] values = ranges.Split(new char[] { ';' }, true);

            foreach (string v in values)

            {

                  coll.Add(IntRange.Parse(v));

            }

            return coll;

      }

      public bool Satisfies(int val)

      {

            foreach (IntRange ir in this)

            {

                  if (ir.InRange(val))

                  {

                        return true;

                  }

            }

                  return false;

      }

}

 

 

So then IntRange.Parse is really the only interesting function.

 

For this I decided to use a regex.  Since I’m not a native speaker of the regex syntax I hacked it up a bit – this is a sample app, right?  J

 

There are 2 regexs.  One for the single number case and one for the number range case.  They can probably be combined if I researched it more.  First I check for the range case and then, if there is not match, I check for the single number case.

 

The method looks roughly like:

 

public static IntRange Parse(string val)

{

      string singleValue = @"^[-]?[0-9]+$";

      string multipleValue = @"^([-]?[0-9]+)-([-]?[0-9]+)$";

      Regex singleReg = new Regex(singleValue, (RegexOptions)0);

      Regex multiReg = new Regex(multipleValue, (RegexOptions)0);

      Match mm = multiReg.Match(val);

      if (mm.Success)

      {

            int lb = int.Parse(mm.Groups[1].ToString());

            int ub = int.Parse(mm.Groups[2].ToString());

            return new IntRange(lb, ub);

      }

      else

      {

            Match sm = singleReg.Match(val);

            if (sm.Success)

            {

                  int lb = int.Parse(sm.Groups[0].ToString());

                  return new IntRange(lb);

            }

      }

      throw new FormatException(string.Format("Error parsing: \"{0}\". Expected string in format \"30\" or \"20-30\""));

}

 

So know we’ve built up our collection of IntRange instances and need to see if an int satisfies the range.  You can easily imagine that code so I won’t bother with it.

 

So there you go – checking a number for a valid range at build time.  Any number of ranges and singe numbers are valid.

 

Really that’s pretty much the whole program.  There is a simple loader that loads the classes that implement ITask, ITaskProperty and IGenerator.  This works off a config file (GenTask.config) – a sample is in the GDN workspace source control.  Then it’s simply a matter of the loading the XML, creating the instances and running IGenerator.Generate.

 

So … that’s about all I’m going to say about MSBuild Task Generator.  We can discuss anything else at the workspace on GDN.  This is where I will provide updates, etc.

 

Let me know what you thought.  What I should have covered.  Why this series sucked.  Etc.

 

I’m recently distracted by tablet and Ink input – so maybe that will be next.  Who knows.