call sum in expression tree - vb.net

I have this query:
Dim test = result.GroupBy(Function(row) groupedindexes.Select(
Function(grpindex) row(grpindex)).ToArray, comp).
Select(Function(g) New groupedresult(g.Key, g.Sum(Function(x) Convert.ToInt32(x(3)))))
At the moment, I'm building this: g.Sum(Function(x) Convert.ToInt32(x(3)))
I have this so far:
Dim groupparameter = Expression.Parameter(GetType(Linq.IGrouping(Of Object(), Object())), "g")
Dim objectparameter = Expression.Parameter(GetType(Object()), "x")
convertMethod = GetType(System.Convert).GetMethod("ToInt32", New Type() {GetType(Object)})
Dim aggregator_expr As LambdaExpression = Expression.Lambda(expression.Call(convertMethod, Expression.ArrayAccess(objectparameter, Expression.Constant(3))), objectparameter)
Dim aggregator_func = GetType(Enumerable).GetMethods(BindingFlags.Public Or BindingFlags.Static).
Where(Function(m) m.Name = "Sum").Where(Function(m) m.ReturnType.FullName = "System.Int32").First
Dim aggregation As Expression = Expression.Call(aggregator_func, groupparameter, aggregator_expr)
At the last row, vs says:
Incorrect number of arguments supplied for call to method 'Int32 Sum(System.Collections.Generic.IEnumerable`1[System.Int32])'
Sure, there is one parameter more. But if I remove the groupparameter, I get another error message. How can I correct this?
Thanks.
EDIT:
here a simple console application:
Imports System.Reflection
Imports System.Linq.Expressions
Module Module1
Dim groupparameter = Expression.Parameter(GetType(Linq.IGrouping(Of Object(), Object())), "g")
Dim objectparameter = Expression.Parameter(GetType(Object()), "x")
Dim convertMethod = GetType(System.Convert).GetMethod("ToInt32", New Type() {GetType(Object)})
Dim aggregator_expr As LambdaExpression = Expression.Lambda(
Expression.Call(convertMethod, Expression.ArrayAccess(objectparameter, Expression.Constant(3))), objectparameter)
Dim aggregator_func = GetType(Enumerable).GetMethods(BindingFlags.Public Or BindingFlags.Static).
Where(Function(m) m.Name = "Sum").Where(Function(m) m.ReturnType.FullName = "System.Int32" AndAlso m.GetParameters.Length = 2)(0).
MakeGenericMethod(GetType(System.Func(Of Object(), Integer)))
Sub Main()
Dim aggregation As Expression = Expression.Call(Nothing, aggregator_func, aggregator_expr, groupparameter)
End Sub
End Module
As you see, I changed a bit the aggregator_func. I would like to call this expression: g.Sum(Function(x) Convert.ToInt32(x(3))).
I have all together, I need only to call the variables in the right order. But I don't get it.
EDIT: The code can simply copy&paste in vs to see, what is wrong.

Firstly, you're looking in Enumerable for methods - that's not a good idea if you're trying to work with expression trees. You should be looking in Queryable.
Next, you need to understand that the Sum method is overloaded - and there are multiple methods which return Int32. You need to check whether the method has two parameters. Note that you can check for multiple conditions in a single Where. For example:
Where(Function(m) m.Name = "Sum" AndAlso
m.ReturnType = GetType(Integer) AndAlso
m.GetParameters().Length = 2)
Hopefully that should at least find you the right method to call. I can't easily tell whether that's all that's wrong (a short but complete program demonstrating what you're trying to do would help) but it should at least get you closer.
EDIT: The parameters you're using are all over the place. For example, you've got an IGrouping<object[], object[]> - that isn't an IEnumerable<Func<object[], int>> - it's an IEnumerable<object[]>. And you're currently specifying the arguments in the wrong order too - the target of an extension method is the first parameter. So here's a short but complete program that doesn't throw an exception:
Imports System.Reflection
Imports System.Linq.Expressions
Class Test
Shared Sub Main()
Dim groupParameter = Expression.Parameter(GetType(Linq.IGrouping(Of Object(), Object())), "g")
Dim objectParameter = Expression.Parameter(GetType(Object()), "x")
Dim convertMethod = GetType(System.Convert).GetMethod("ToInt32", New Type() {GetType(Object)})
Dim aggregatorExpr As LambdaExpression = Expression.Lambda(
Expression.Call(
convertMethod,
Expression.ArrayAccess(objectParameter, Expression.Constant(3))
), objectParameter)
Dim aggregatorFunc = GetType(Enumerable) _
.GetMethods(BindingFlags.Public Or BindingFlags.Static) _
.Where(Function(m) m.Name = "Sum") _
.Where(Function(m) m.ReturnType.FullName = "System.Int32") _
.Where(Function(m) m.GetParameters.Length = 2)(0) _
.MakeGenericMethod(GetType(Object()))
Dim aggregation As Expression = Expression.Call(
Nothing,
aggregatorFunc,
groupParameter,
aggregatorExpr)
End Sub
End Class

Related

Runtime Generated EventHandler of Unknown Type

Ok, so I have been stuck on this for a while, combining various question solutions from on here.
Situation:
I instantiate an Object of Type x by using the activator.CreateInstance(x)
Type x can vary but always has the same properties and methods.
The problem is that type x has an event ReadCompleted as ReadCompleted (this is a delegate), type y has the same event, with the same delegate but it is of type ReadCompleted1, z => ReadCompleted2, etc...
Solution:
I assign a delegate at runtime using the following code:
Dim Client As Object = ServiceProvider.GetService(TypeDict(Entity))
Dim TaskCompletionSource As New TaskCompletionSource(Of Entity)()
Dim addMethod As MethodInfo = Client.GetType().GetMethod("add_ReadCompleted")
Dim removeMethod As MethodInfo = Client.GetType().GetMethod("remove_ReadCompleted")
Dim self As Object = Nothing
Dim getSelf As Func(Of Object) = Function() self
Dim eventArgType As Type = Client.GetType().GetEvent("ReadCompleted").EventHandlerType.GetMethod("Invoke").GetParameters()(1).ParameterType
Dim e As ParameterExpression = Expression.Variable(eventArgType)
Dim ExpBlock As BlockExpression = Expression.Block({Expression.Parameter(GetType(Object)), e},
Expression.Call(
Nothing,
Me.GetType().GetMethod("ProcessTask"),
Expression.Convert(e, eventArgType),
Expression.Constant(TaskCompletionSource)),
Expression.Call(
Expression.Constant(Client),
removeMethod,
Expression.Convert(
Expression.Invoke(
Expression.Constant(getSelf)),
addMethod.GetParameters()(0).ParameterType)
)
)
Dim lambda As LambdaExpression = Expression.Lambda(
addMethod.GetParameters()(0).ParameterType,
ExpBlock,
Expression.Parameter(GetType(Object)),
Expression.Parameter(eventArgType))
Dim dlg As [Delegate] = lambda.Compile()
self = dlg
addMethod.Invoke(Client, {dlg})
Client.ReadAsync(PrimaryKey)
Now my knowledge on linq and it's Expression class is limited and i did my best to research the msdn documentation but i can't figure it out:
the method ProcessTask gets called properly, for which i've worked long enough, but my parameter e is always Nothing, resulting in a NullReferenceException.
The method:
Public Shared Sub ProcessTask(ByVal e, ByRef tcs)
'Console.WriteLine(e.GetType())
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
I have no idea why, according to how i see it this is the correct way to call my method, but obviously it isn't. Can someone point me in the right direction for this?
Could be I'm just plain blind and missing something very obvious....
Thanks in advance!
EDIT:
Whilst awaiting an answer i tried to do some more debugging(which is hell in this scenario) saw that if i do:
Dim s As ParameterExpression = Expression.Variable(GetType(Object), "Sender")
Dim ExpBlock As BlockExpression = Expression.Block({s, e},
Expression.Call(
Nothing,
GetType(Console).GetMethod("WriteLine", New Type() {GetType(String)}),
Expression.Call(s, GetType(Object).GetMethod("ToString"))),
Expression.Call(
Expression.Constant(Client),
removeMethod,
Expression.Convert(
Expression.Invoke(
Expression.Constant(getSelf)),
addMethod.GetParameters()(0).ParameterType)
))
The sender as object parameter is also nothing, so i'm getting the feeling that none of my arguments are getting passed through...
Hope this helps shed more light on the matter.
I figured it out, turns out i misinterpreted the whole parameter thing of Expression:
Dim Client As Object = ServiceProvider.GetService(TypeDict(Entity))
Dim TaskCompletionSource As New TaskCompletionSource(Of Entity)()
Dim addMethod As MethodInfo = Client.GetType().GetMethod("add_ReadCompleted")
Dim removeMethod As MethodInfo = Client.GetType().GetMethod("remove_ReadCompleted")
Dim self As Object = Nothing
Dim getSelf As Func(Of Object) = Function() self
Dim eventArgType As Type = Client.GetType().GetEvent("ReadCompleted").EventHandlerType.GetMethod("Invoke").GetParameters()(1).ParameterType
Dim s As ParameterExpression = Expression.Parameter(GetType(Object), "Sender")
Dim e As ParameterExpression = Expression.Variable(eventArgType, "EventArgs")
Dim ExpBlock As BlockExpression = Expression.Block(\*{Expression.Parameter('GetType(Object)), e},*\ <--- this has to go
Expression.Call(
Nothing,
Me.GetType().GetMethod("ProcessTask"),
Expression.Convert(e, eventArgType),
Expression.Constant(TaskCompletionSource)),
Expression.Call(
Expression.Constant(Client),
removeMethod,
Expression.Convert(
Expression.Invoke(
Expression.Constant(getSelf)),
addMethod.GetParameters()(0).ParameterType)
)
)
Dim lambda As LambdaExpression = Expression.Lambda(
addMethod.GetParameters()(0).ParameterType,
ExpBlock,
s,
e)
\*Expression.Parameter(GetType(Object)),
Expression.Parameter(eventArgType))*\ <-- this has to change
Dim dlg As [Delegate] = lambda.Compile()
self = dlg
addMethod.Invoke(Client, {dlg})
Client.ReadAsync(PrimaryKey)
So as i see it the parameters don't have to be defined in the expression itself, but they sould be created, and the reference should be kept to use them in the lambda in which you compile the expression.
Hope someone in the future has some use for this answer, and thanks to all for taking a look at this.

Setting Values Using Reflection

I am using VB.NET. I have created a small scale test project that works similar to my program. I am trying to say something like: getObjectType(object1) if Object1.getType() = "ThisType" then get the properties. Each object contains an ID and I would like to do this: Object1.Id = -1 (I know it won't be that short or easy). I thought there was a way to do this by using something like: Object1.SetValue(Value2Change, NewValue) but that doesn't work and I'm not sure how to exactly do this. Below is my code. Thank You!
Module Module1
Sub Main()
Dim Db As New Luk_StackUp_ProgramEntities
Dim Obj1 As IEnumerable(Of Stackup) = (From a In Db.Stackups).ToList
Dim Obj2 As IEnumerable(Of Object) = (From a In Db.Stackups).ToList
Dim IdNow As Integer = Obj1(0).IdStackup
Dim StackUpNow As Stackup = (From a In Db.Stackups Where a.IdStackup = IdNow).Single
Console.WriteLine(StackUpNow)
getInfo(StackUpNow)
getInfo(Obj1(0), Obj1(0))
areObjectsSame(Obj1(0), Obj1(67))
switchObjects(Obj1(0), Obj2(1))
getObjectValues(Obj2(55))
Console.WriteLine("========================================")
TestCopyObject(StackUpNow)
ChangeObjectValues(StackUpNow)
Console.ReadKey()
End Sub
Private Sub ChangeObjectValues(Object1 As Object)
Console.WriteLine("Changing Object Values")
Dim myField As PropertyInfo() = Object1.GetType().GetProperties()
'Dim Index As Integer 'Did not find value
'For Index = 0 To myField.Length - 1
' If myField(Index).ToString.Trim = "IdStackup" Then
' Console.WriteLine("Found the ID")
' End If
'Next
If Object1.GetType().Name = "Stackup" Then
'Set the Value
End If
End Sub
You can use PropertyInfo.SetValue to set the value using reflection. You could also use a LINQ SingleOrDefault query to simplify finding the correct PropertyInfo, so you could do something like this:
Private Sub ChangeObjectValues(Object1 As Object)
Console.WriteLine("Changing Object Values")
Dim t As Type = Object1.GetType()
If t.Name = "Stackup" Then
Dim myField As PropertyInfo = t.GetProperties() _
.SingleOrDefault(Function(x) x.Name = "IdStackup")
If myField IsNot Nothing Then
Console.WriteLine("Found the ID")
myField.SetValue(Object1, -1)
End If
End If
End Sub
It's not clear from the question if you really need to use reflection - perhaps a common interface to define the id property, or just type checking and casting, etc. would be better.
Well, I struggled to see how your code example applied to your question, but if you are simply asking how to set the ID of an object using reflection, this code might help you. The trick is that a property is typically handled using a set and a get method.
Imports System.Web.UI.WebControls
Imports System.Reflection
Module Module1
Sub Main()
Dim tb As New Label()
Dim t As Type = tb.GetType()
If TypeOf tb Is Label Then
Dim mi As MethodInfo = t.GetMethod("set_ID")
mi.Invoke(tb, New Object() {"-1"})
End If
Console.WriteLine(tb.ID)
Console.ReadLine()
End Sub
End Module

expr. tree: Static method requires null instance, non-static method requires non-null instance

I searched the questions and found some topics and I suspect the error cause, but I can't figure it out.
I would like to build this expression part:
Function(row) groupedindexes.Select(
Function(grpindex) row(grpindex))
I already build the part Function(grpindex) row(grpindex) with this:
Dim fieldselector As Expressions.LambdaExpression
fieldselector = Expression.Lambda(Expression.ArrayAccess(rowParameter, indexParameter), indexParameter)
The declarations are:
Dim rowParameter = Expression.Parameter(GetType(Object()), "Row")
Dim indexParameter = Expression.Parameter(GetType(Integer), "grpindex")
Now, I would like to build the Select part like this:
Dim outerfieldselector As Expressions.LambdaExpression
outerfieldselector = Expression.Lambda(Expression.Call(grpindexes, selectMethod, fieldselector), rowParameter)
The declarations are:
Dim grpindexes As Expression = Expression.Constant(groupedindexes, GetType(System.Collections.Generic.List(Of Integer)))
Dim selectMethod = GetType(Queryable).GetMethods(BindingFlags.Public Or BindingFlags.Static).First(Function(m) m.Name = "Select").MakeGenericMethod(GetType(Object), GetType(System.Func(Of Integer, Object)))
groupedindexes is a normal List(Of Integer).
In runtime, I get the above error at the line outerfieldselector=...
In my opinion, it should work. I call the Select method on grpindexes with one argument (fieldselector).
What could be the problem?
Thanks.
EDIT: a sample project can be downloaded at this link: http://www.filedropper.com/exptree
EDIT II:
here a simple, short console application project:
Imports System.Reflection
Imports System.Linq.Expressions
Module Module1
Dim rowParameter = Expression.Parameter(GetType(Object()), "Row")
Dim indexParameter = Expression.Parameter(GetType(Integer), "grpindex")
Dim expr As Expression = Nothing
Dim groupedindexes As New List(Of Integer)
Dim grpindexes As Expression = Expression.Constant(groupedindexes, GetType(System.Collections.Generic.List(Of Integer)))
Dim selectMethod = GetType(Queryable).GetMethods(BindingFlags.Public Or BindingFlags.Static).First(
Function(m) m.Name = "Select").MakeGenericMethod(GetType(Object), GetType(System.Func(Of Integer, Object)))
Dim fieldselector As Expressions.LambdaExpression
Dim outerfieldselector As Expressions.LambdaExpression
Sub Main()
groupedindexes.Add(0)
groupedindexes.Add(1)
groupedindexes.Add(2)
fieldselector = Expression.Lambda(Expression.ArrayAccess(rowParameter, indexParameter), indexParameter)
outerfieldselector = Expression.Lambda(Expression.Call(grpindexes, selectMethod, fieldselector), rowParameter)
End Sub
End Module
EDIT 3:
I think, I got it with the help of svick.
Dim selectMethod = GetType(Enumerable).GetMethods(BindingFlags.Public Or BindingFlags.Static).First(Function(m) m.Name = "Select").MakeGenericMethod(GetType(Integer), GetType(Object))
The problem is that Queryable.Select() is not an instance method. You call it as if it was one in VB, but it's not, and that's reflected in expression trees.
So, the line should look like this:
outerfieldselector = Expression.Lambda(Expression.Call(Nothing, selectMethod, grpindexes, fieldselector), rowParameter)
Even if you fix that, your code still won't work. Some of the issues are:
MakeGenericMethod() expects the types of the type parameters. Here those are TSource and TResult, which should be Integer and Object.
List(Of Integer) does not implement IQueryable.

expression.call value cannot be null

I'm trying to code this LINQ query with expression trees:
Result = Result.Where(Function(Row) Convert.ToInt32(Row(2)) <= 10)
Result is declared as Dim Result As IEnumerable(Of Object()).
I have this code so far:
Dim queryiabledata As IQueryable(Of Object()) = Result.AsQueryable
Dim pe As ParameterExpression = Expression.Parameter(GetType(String), "Row(2)")
Dim left As expression = Expression.Call(pe, GetType(String).GetMethod("Convert.ToInt32", System.Type.EmptyTypes))
Dim right As Expression = Expression.Constant(10)
Dim e1 As Expression = Expression.LessThanOrEqual(left, right)
Dim predicatebody As Expression = e1
Dim wherecallexpression As MethodCallExpression = Expression.Call(
GetType(Queryable), "Where", New Type() {queryiabledata.ElementType}, queryiabledata.Expression,
Expression.Lambda(Of Func(Of Object(), Boolean))(predicatebody, New ParameterExpression() {pe}))
Result = queryiabledata.Provider.CreateQuery(Of Object())(wherecallexpression)
But if I run the query, I get an ArgumentNullException (Value cannot be null. Parameter name: method) at Expression.Call.
I tried to change "Convert.ToInt32" to "Value", but I got the same error.
How can I fix it?
Are the another code lines right to get the desired result?
I'm more used to C#, though I do VB.NET occasionally. Reflection in VB.NET is downright ugly. Getting the Where method is a bit of a hack. Here's the code:
shortForm and longForm should be identical.
Dim result As IEnumerable(Of Object()) = New List(Of Object())()
Dim queryiabledata As IQueryable(Of Object()) = result.AsQueryable()
Dim shortForm As Expression = queryiabledata.Where(Function(Row) Convert.ToInt32(Row(2)) <= 10).Expression
Dim whereMethod = GetType(Queryable).GetMethods(BindingFlags.Public Or BindingFlags.Static).
First(Function(m) m.Name = "Where").
MakeGenericMethod(GetType(Object()))
Dim convertMethod = GetType(System.Convert).GetMethod("ToInt32", New Type() {GetType(Object)})
Dim rowParameter = Expression.Parameter(GetType(Object()), "Row")
Dim longform As Expression =
Expression.Call(
whereMethod,
queryiabledata.Expression,
Expression.Lambda(
Expression.LessThanOrEqual(
Expression.Call(
convertMethod,
Expression.ArrayAccess(
rowParameter,
Expression.Constant(2)
)
),
Expression.Constant(10)
),
rowParameter
)
)
result = queryiabledata.Provider.CreateQuery(longform)
This line seems suspect to me:
GetType(String).GetMethod("Convert.ToInt32", System.Type.EmptyTypes)
GetType(String) returns the runtime type String. You then try to get a method called "Convert.ToInt32" which doesn't exist on the string type. I suspect that is returning null which is the source of your exception.
Perhaps you need to use something like this:
GetType(Convert).GetMethod("ToInt32", new Type() {GetType(Object)})
Since there are multiple overloads of the ToInt32 method of the Convert class, you need to specify which of the overloads you want by providing an array of Type as the second parameter. In other words, you a saying "give me the overload that takes an Object type as it's parameter".

Parsing latin plant names with piglet fluent configuration

I have the following tests.And Classes. Now I just need to find how to write the rules and it seemed so simple ;-). But I'm going nowhere fast. As the tags say, I would like to use and learn piglet for this.
Public Class Plant
Public Property Genus As String
Public Property Species As String
Public Property SubSpecies As String
Public Property IsHybrid As Boolean
End Class
Public Class ParserTests
<Test>
Public Sub IfGenusCanBeFoundWhenOnlyGenusAndSpiecesAreThere()
Dim parser = New ParseLatinPlantName
Dim result = parser.Parse("Salvia sylvatica")
Assert.AreEqual("Salvia", result.Genus)
End Sub
<Test>
Public Sub IfSpeciesCanBeFoundWhenOnlyGenusAndSpiecesAreThere()
Dim parser = New ParseLatinPlantName
Dim result = parser.Parse("Salvia sylvatica")
Assert.AreEqual("sylvatica", result.Species)
End Sub
<Test>
Public Sub IfSubSpeciesCanBeFoundWhenSubSpeciesIsProvided()
Dim parser = New ParseLatinPlantName
Dim result = parser.Parse("Salvia sylvatica sp. crimsonii")
Assert.AreEqual("crimsonii", result.SubSpecies)
End Sub
<Test>
Public Sub IfIsHybridIsTrueWhenxIsInNameCanBeFoundWhenSubSpeciesIsProvided()
Dim parser = New ParseLatinPlantName
Dim result = parser.Parse("Salvia x jamensis")
Assert.IsTrue(result.IsHybrid)
End Sub
End Class
And here is what I tried so far.
Public Class ParseLatinPlantName
Public Function Parse(ByVal name As String) As Plant
Dim config = ParserFactory.Fluent()
Dim expr = config.Rule()
Dim name1 = config.Expression()
name1.ThatMatches("[a-z]+").AndReturns(Function(f) f)
Dim space1 = config.Expression()
space1.ThatMatches(" ").AndReturns(Function(f) f)
expr.IsMadeUp.By(name).As("Genus").Followed.By(name).As("Species").WhenFound(Function(f) New Plant With {.Genus = f.Genus})
Dim parser = config.CreateParser()
Dim result = DirectCast(parser.Parse(name), Plant)
Return result
End Function
End Class
Update
I got the first two tests passing thanks to Randompunter.
Public Class ParseLatinPlantName
Public Function Parse(ByVal name As String) As Plant
Dim config = ParserFactory.Fluent()
Dim expr = config.Rule()
Dim name1 = config.Expression()
name1.ThatMatches("\w+").AndReturns(Function(f) f)
expr.IsMadeUp.By(name1).As("Genus") _
.Followed.By(name1).As("Species") _
.WhenFound(Function(f) New Plant With {.Genus = f.Genus, .Species = f.Species})
Dim parser = config.CreateParser()
Dim result = DirectCast(parser.Parse(name), Plant)
Return result
End Function
End Class
Firstly, your original (then corrected expression matched only lowercase letters). This was corrected by changing it to \w+ which matched any other letter.
You second two tests failed because your grammar does not allow for more than two following letters. You will need to add a rule to make this work.
For instance, you have an example where a subspecies is provided. Assume that this takes the form where .sp xxx is an optional thing to pass, a separate rule needs to provided for this.
This passes the test for an optional subspecies
Public Class ParseLatinPlantName
Public Function Parse(ByVal name As String) As Plant
Dim config = ParserFactory.Fluent()
Dim expr = config.Rule()
Dim subSpecies = config.Rule()
Dim sp = config.Expression()
sp.ThatMatches("sp\.").AndReturns(Function(f) f)
Dim name1 = config.Expression()
name1.ThatMatches("\w+").AndReturns(Function(f) f)
Dim nothing1 = config.Rule()
expr.IsMadeUp.By(name1).As("Genus") _
.Followed.By(name1).As("Species") _
.Followed.By(subSpecies).As("Subspecies") _
.WhenFound(Function(f) New Plant With {.Genus = f.Genus, .Species = f.Species, .SubSpecies = f.Subspecies})
subSpecies.IsMadeUp.By(sp).Followed.By(name1).As("Subspecies").WhenFound(Function(f) f.Subspecies) _
.Or.By(nothing1)
Dim parser = config.CreateParser()
Dim result = DirectCast(parser.Parse(name), Plant)
Return result
End Function
End Class
Excuse my probably extremely shoddy VB, it was ages ago. Note that there is an expression that explicitly matches "sp." to distinguish it from any other type of name. This rule is then also matched by another rule that matches nothing. This enables the subspecies part to be optional.
I'm not to sure what you want parsed from the hybrid rule. I assume it must be something with name followed by an x and followed by some other name then it is a hybrid. To match this, add another rule to your parser.
The following parser passes all of your tests:
Public Class ParseLatinPlantName
Public Function Parse(ByVal name As String) As Plant
Dim config = ParserFactory.Fluent()
Dim expr = config.Rule()
Dim subSpecies = config.Rule()
Dim hybridIndicator = config.Expression
hybridIndicator.ThatMatches("x").AndReturns(Function(f) f)
Dim sp = config.Expression()
sp.ThatMatches("sp\.").AndReturns(Function(f) f)
Dim name1 = config.Expression()
name1.ThatMatches("\w+").AndReturns(Function(f) f)
Dim nothing1 = config.Rule()
expr.IsMadeUp.By(name1).As("Genus") _
.Followed.By(name1).As("Species") _
.Followed.By(subSpecies).As("Subspecies") _
.WhenFound(Function(f) New Plant With {.Genus = f.Genus, .Species = f.Species, .SubSpecies = f.Subspecies}) _
.Or.By(name1).As("FirstSpecies").Followed.By(hybridIndicator).Followed.By(name1).As("SecondSpecies") _
.WhenFound(Function(f) New Plant With {.IsHybrid = True})
subSpecies.IsMadeUp.By(sp).Followed.By(name1).As("Subspecies").WhenFound(Function(f) f.Subspecies) _
.Or.By(nothing1)
Dim parser = config.CreateParser()
Dim result = DirectCast(parser.Parse(name), Plant)
Return result
End Function
End Class
It is important that your expressions if they overlap are declared in the order of precedence. If you were to declare name1 before hybridIndicator the x would be recognized as a name, causing the parsing to fail. And as you probably noticed, Piglet ignores whitespace by default, there is no need to make a rule for it. If this setting is not desired, there is an option to turn it off in the configurator. (use the Ignore method)