Nearly working version

This commit is contained in:
Pieter Vander Vennet 2020-05-02 13:09:49 +02:00
parent 2c2a28d30a
commit 62584c9189
94 changed files with 4011 additions and 4143 deletions

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AspectedRouting\AspectedRouting.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,63 @@
using System.Collections.Generic;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using Xunit;
namespace AspectedRouting.Test
{
public class FunctionsTest
{
private IExpression MustMatchJson()
{
var json = "{" +
"\"name\":\"test\"," +
"\"description\":\"test\"," +
"\"$mustMatch\":{\"a\":\"b\",\"x\":\"y\"}}";
return JsonParser.AspectFromJson(new Context(), json, "test.json");
}
[Fact]
public void TestAll_AllTags_Yes()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
{"x", "y"}
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
var result = expr.Evaluate(new Context());
Assert.Equal("yes", result);
}
[Fact]
public void TestAll_NoMatch_No()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
var result = expr.Evaluate(new Context());
Assert.Equal("no", result);
}
[Fact]
public void TestAll_NoMatchDifferent_No()
{
var tagsAx = new Dictionary<string, string>
{
{"a", "b"},
{"x", "someRandomValue"}
};
var expr = new Apply(MustMatchJson(), new Constant(tagsAx)).Optimize();
var result = expr.Evaluate(new Context());
Assert.Equal("no", result);
}
}
}

View file

@ -0,0 +1,84 @@
using System.Collections.Generic;
using AspectedRouting.IO.itinero1;
using AspectedRouting.Language;
using AspectedRouting.Language.Functions;
using Xunit;
namespace AspectedRouting.Test
{
public class LuaPrinterTest
{
[Fact]
public void ToLua_SimpleMapping_Table()
{
var mapping = new Mapping(
new[] {"a", "b", "c"},
new[]
{
new Constant(5),
new Constant(6),
new Constant(7),
}
);
var luaPrinter = new LuaPrinter(new Context());
var result = luaPrinter.MappingToLua(mapping);
Assert.Equal(
"{\n a = 5,\n b = 6,\n c = 7\n}"
, result);
}
[Fact]
public void ToLua_NestedMapping_Table()
{
var mapping = new Mapping(
new[] {"a"},
new[]
{
new Mapping(new[] {"b"},
new[]
{
new Constant(42),
}
)
}
);
var luaPrinter = new LuaPrinter(new Context());
var result = luaPrinter.MappingToLua(mapping);
Assert.Equal("{\n a = {\n b = 42\n }\n}", result);
}
[Fact]
public void Sanity_EveryBasicFunction_HasDescription()
{
var missing = new List<string>();
foreach (var (_, f) in Funcs.Builtins)
{
if (string.IsNullOrEmpty(f.Description))
{
missing.Add(f.Name);
}
}
Assert.True(0 == missing.Count,
"These functions do not have a description: " + string.Join(", ", missing));
}
[Fact]
public void Sanity_EveryBasicFunction_HasArgNames()
{
var missing = new List<string>();
foreach (var (_, f) in Funcs.Builtins)
{
if (f.ArgNames == null)
{
missing.Add(f.Name);
}
}
Assert.True(0 == missing.Count,
"These functions do not have a description: " + string.Join(", ", missing));
}
}
}

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Functions;
using Xunit;
namespace AspectedRouting.Test
{
public class TestAnalysis
{
[Fact]
public void OnAll_SmallTagSet_AllCombinations()
{
var possibleTags = new Dictionary<string, List<string>>
{
{"a", new List<string> {"x", "y"}}
};
var all = possibleTags.OnAllCombinations(dict => ObjectExtensions.Pretty(dict), new List<string>()).ToList();
Assert.Equal(3, all.Count);
Assert.Contains("{}", all);
Assert.Contains("{a=x;}", all);
Assert.Contains("{a=y;}", all);
}
[Fact]
public void OnAll_TwoTagSet_AllCombinations()
{
var possibleTags = new Dictionary<string, List<string>>
{
{"a", new List<string> {"x", "y"}},
{"b", new List<string> {"u", "v"}}
};
var all = possibleTags.OnAllCombinations(dict => dict.Pretty(), new List<string>()).ToList();
Assert.Equal(9, all.Count);
Assert.Contains("{}", all);
Assert.Contains("{a=x;}", all);
Assert.Contains("{a=y;}", all);
Assert.Contains("{b=u;}", all);
Assert.Contains("{b=v;}", all);
Assert.Contains("{a=x;b=u;}", all);
Assert.Contains("{a=x;b=v;}", all);
Assert.Contains("{a=y;b=u;}", all);
Assert.Contains("{a=y;b=v;}", all);
}
}
}

View file

@ -0,0 +1,182 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Xunit;
namespace AspectedRouting.Test
{
public class TestInterpreter
{
[Fact]
public void MaxSpeedAspect_Evaluate_CorrectMaxSpeed()
{
var json =
"{\"name\": \"legal_maxspeed_be\",\"description\": \"Gives, for each type of highway, which the default legal maxspeed is in Belgium. This file is intended to be reused for in all vehicles, from pedestrian to car. In some cases, a legal maxspeed is not really defined (e.g. on footways). In that case, a socially acceptable speed should be taken (e.g.: a bicycle on a pedestrian path will go say around 12km/h)\",\"unit\": \"km/h\",\"$max\": {\"maxspeed\": \"$parse\",\"highway\": {\"residential\": 30},\"ferry\":5}}";
var aspect = JsonParser.AspectFromJson(null, json, null);
var tags = new Dictionary<string, string>
{
{"maxspeed", "42"},
{"highway", "residential"},
{"ferry", "yes"}
};
Assert.Equal("tags -> double", string.Join(", ", aspect.Types));
Assert.Equal(42d, new Apply(aspect, new Constant(tags)).Evaluate(null));
}
[Fact]
public void MaxSpeed_AnalyzeTags_AllTagsReturned()
{
var json =
"{\"name\": \"legal_maxspeed_be\",\"description\": \"Gives, for each type of highway, which the default legal maxspeed is in Belgium. This file is intended to be reused for in all vehicles, from pedestrian to car. In some cases, a legal maxspeed is not really defined (e.g. on footways). In that case, a socially acceptable speed should be taken (e.g.: a bicycle on a pedestrian path will go say around 12km/h)\",\"unit\": \"km/h\",\"$max\": {\"maxspeed\": \"$parse\",\"highway\": {\"residential\": 30},\"ferry\":5}}";
var aspect = JsonParser.AspectFromJson(null, json, null);
Assert.Equal(
new Dictionary<string, List<string>>
{
{"maxspeed", new List<string>()},
{"highway", new List<string> {"residential"}},
{"ferry", new List<string>()}
},
aspect.PossibleTags());
}
[Fact]
public void EitherFunc_Const_Id_Specialized_Has_Correct_Type()
{
var b = new Var("b");
var c = new Var("c");
var eitherFunc = new Apply(Funcs.Const, Funcs.Id);
Assert.Single(eitherFunc.Types);
Assert.Equal(new Curry(b,
new Curry(c, c)), eitherFunc.Types.First());
var (_, (func, arg)) = eitherFunc.FunctionApplications.First();
// func == const : (c -> c) -> b -> (c -> c)
var funcType = new Curry(new Curry(c, c),
new Curry(b, new Curry(c, c)));
Assert.Equal(funcType, func.Types.First());
// arg == id (but specialized): (c -> c)
var argType = new Curry(c, c);
Assert.Equal(argType, arg.Types.First());
}
[Fact]
public void EitherFunc_SpecializeToString_Const()
{
var a = new Constant("a");
var mconst = new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.Const);
var specialized = new Apply(mconst, a).Specialize(Typs.String);
Assert.Equal("((($const $id) $const) \"a\")", specialized.ToString());
Assert.Equal("string; $b -> string", string.Join("; ", new Apply(mconst, a).Types));
Assert.Equal("\"a\"", specialized.Evaluate(null).Pretty());
Assert.Equal("\"a\"", new Apply(new Apply(mconst, a), new Constant("42")).Specialize(Typs.String)
.Evaluate(null)
.Pretty());
}
[Fact]
public void Parse_Five_5()
{
var str = new Constant("5");
var parsed = new Apply(Funcs.Parse, str).Specialize(Typs.Double);
var o = parsed.Evaluate(null);
Assert.Equal(5d, o);
}
[Fact]
public void Concat_TwoString_AB()
{
var a = new Constant("a");
var b = new Constant("b");
Assert.Equal("string -> string -> string", Funcs.Concat.Types.First().ToString());
var ab = Funcs.Concat.Apply(a, b);
Assert.Equal("(($concat \"a\") \"b\")", ab.ToString());
Assert.Equal("ab", ab.Evaluate(null));
}
[Fact]
public void Id_Evaluate_ReturnsInput()
{
var a = new Constant("a");
var aId = Funcs.Id.Apply(a);
Assert.Equal("\"a\" : string", aId + " : " + string.Join(", ", aId.Types));
Assert.Equal("a", aId.Evaluate(null));
}
[Fact]
public void MaxTest()
{
var ls = new Constant(new ListType(Typs.Double),
new[] {1.1, 2.0, 3.0}.Select(d => (object) d));
Assert.Equal("[1.1, 2, 3] : list (double)",
ls.Evaluate(null).Pretty() + " : " + string.Join(", ", ls.Types));
var mx = Funcs.Max.Apply(ls);
Assert.Equal("($max [1.1, 2, 3]) : double", mx + " : " + string.Join(", ", mx.Types));
Assert.Equal(3d, mx.Evaluate(null));
var mxId = Funcs.Id.Apply(mx);
// identity function is not printed
Assert.Equal("($max [1.1, 2, 3]) : double", mxId + " : " + string.Join(", ", mxId.Types));
Assert.Equal(3d, mxId.Evaluate(null));
}
[Fact]
public void Fresh_SomeType_NewVarName()
{
Assert.Equal("$d", Var.Fresh(
Curry.ConstructFrom(new Var("a"),
new Var("b"),
new Var("b"),
new Var("c"),
new Var("aa"))).Name);
}
[Fact]
public void Mapping_Evaluate_CorrectMapping()
{
var mapping = Mapping.Construct(
("a", new Constant(5.0)),
("b", new Constant(-3))
);
Assert.Equal("5", mapping.Evaluate(null, new Constant("a")).Pretty());
Assert.Equal("-3", mapping.Evaluate(null, new Constant("b")).Pretty());
}
[Fact]
public void TestStringGeneration()
{
var v = Var.Fresh(new HashSet<string> {"$a", "$b"});
Assert.Equal("$c", v.Name);
}
[Fact]
public void TestDeconstruct()
{
var a = new Constant("a");
var app = new Apply(
new Apply(
new Apply(Funcs.Id, Funcs.Id), Funcs.Id), a);
var (f, args ) = app.DeconstructApply().Value;
Assert.Equal(Funcs.Id.Name, ((Function) f).Name);
Assert.Equal(new List<IExpression> {Funcs.Id, Funcs.Id, a}.Select(e => e.ToString()),
args.Select(e => e.ToString()));
}
}
}

22
AspectedRouting.sln Normal file
View file

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspectedRouting", "AspectedRouting\AspectedRouting.csproj", "{4A53D888-6872-4E5D-8022-338CC302473F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspectedRouting.Test", "AspectedRouting.Test\AspectedRouting.Test.csproj", "{A1309041-8AAE-42D7-A886-94C9FFC6A28C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4A53D888-6872-4E5D-8022-338CC302473F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A53D888-6872-4E5D-8022-338CC302473F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A53D888-6872-4E5D-8022-338CC302473F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A53D888-6872-4E5D-8022-338CC302473F}.Release|Any CPU.Build.0 = Release|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1309041-8AAE-42D7-A886-94C9FFC6A28C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,99 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Typ;
namespace AspectedRouting.Functions
{
public class MemberOf : Function
{
public override string Description { get; } =
"This function uses memberships of relations to calculate values.\n" +
"\n" +
"Consider all the relations the scrutinized way is part of." +
"The enclosed function is executed for every single relation which is part of it, generating a list of results." +
"This list of results is in turn returned by 'memberOf'" +
"\n" +
"In itinero 1/lua, this is implemented by converting the matching relations and by adding the tags of the relations to the dictionary (or table) with the highway tags." +
"The prefix is '_relation:n:key=value', where 'n' is a value between 0 and the number of matching relations (implying that all of these numbers are scanned)." +
"The matching relations can be extracted by the compiler for the preprocessing.\n\n" +
"For testing, the relation can be emulated by using e.g. '_relation:0:key=value'";
public override List<string> ArgNames { get; } = new List<string>
{
"f","tags"
};
public MemberOf() : base(
"memberOf", true,
new[]
{
new Curry(
new Curry(Typs.Tags, new Var("a")),
new Curry(Typs.Tags, new ListType(new Var("a"))))
}
)
{
}
public MemberOf(IEnumerable<Type> types) : base("memberOf", types)
{
}
public override object Evaluate(Context c, params IExpression[] arguments)
{
var f = arguments[0];
var tags = (Dictionary<string, string>) arguments[1].Evaluate(c);
var prefixes = new Dictionary<int, Dictionary<string, string>>();
foreach (var (k, v) in tags)
{
if (!k.StartsWith("_relation:")) continue;
var s = k.Split(":")[1];
if (int.TryParse(s, out var i))
{
var key = k.Substring(("_relation:" + i + ":").Length);
if (prefixes.TryGetValue(i, out var relationTags))
{
relationTags[key] = v;
}
else
{
prefixes[i] = new Dictionary<string, string>
{
{key, v}
};
}
}
}
// At this point, we have all the tags of all the relations
// Time to run the function on all of them
var result = new List<object>();
foreach (var relationTags in prefixes.Values)
{
var o = f.Evaluate(c, new Constant(relationTags));
if (o != null)
{
result.Add(o);
}
}
return result;
}
public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{
var unified = Types.SpecializeTo(allowedTypes);
if (unified == null)
{
return null;
}
return new MemberOf(unified);
}
}
}

View file

@ -1,42 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
namespace AspectedRouting.IO
{
public static class JsonPrinter
{
private static string Objs(params (string k, string v)[] stuff)
{
return "{\n" + string.Join("\n", stuff.Where(kv => kv.v != null).Select(kv => kv.k.Dq() + ": " + kv.v)).Indent() + "\n}";
}
private static string Arr(IEnumerable<string> strs)
{
return "[" + string.Join(", ", strs) + "]";
}
private static string Dq(this string s)
{
if (s == null)
{
return null;
}
return '"' + s + '"';
}
public static string Print(AspectMetadata meta)
{
return Objs(
("name", meta.Name.Dq()),
("description", meta.Description.Dq()),
("unit", meta.Unit.Dq()),
("author", meta.Author.Dq()),
("file", meta.Filepath.Dq()),
("type", Arr(meta.Types.Select(tp => tp.ToString().Dq()))),
("value", meta.ExpressionImplementation.ToString())
);
}
}
}

View file

@ -1,535 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using static AspectedRouting.Deconstruct;
namespace AspectedRouting.IO
{
public class LuaPrinter
{
public static Dictionary<string, string> BasicFunctions = _basicFunctions();
private readonly HashSet<string> _deps = new HashSet<string>();
private readonly List<string> _code = new List<string>();
private readonly HashSet<string> _neededKeys = new HashSet<string>();
/// <summary>
/// A dictionary containing the implementation of basic functions
/// </summary>
/// <returns></returns>
private static Dictionary<string, string> _basicFunctions()
{
var imps = new Dictionary<string, string>();
var functionsToFetch = Funcs.BuiltinNames.ToList();
// These functions should be loaded from disk, but are not necessarily included
functionsToFetch.Add("table_to_list");
functionsToFetch.Add("debug_table");
functionsToFetch.Add("unitTest");
functionsToFetch.Add("unitTestProfile");
functionsToFetch.Add("double_compare");
foreach (var name in functionsToFetch)
{
var path = $"IO/lua/{name}.lua";
if (File.Exists(path))
{
imps[name] = File.ReadAllText(path);
}
}
return imps;
}
public LuaPrinter()
{
_deps.Add("debug_table");
}
private string ToLua(IExpression bare, Context context, string key = "nil")
{
var collectedMapping = new List<IExpression>();
var order = new List<IExpression>();
if (UnApply(
UnApply(
IsFunc(Funcs.FirstOf),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
_deps.Add(Funcs.FirstOf.Name);
return "first_match_of(tags, result, \n" +
" " + ToLua(order.First(), context, key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First(), context)).Indent().Indent() +
")";
}
if (UnApply(
UnApply(
IsFunc(Funcs.MustMatch),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
_deps.Add(Funcs.MustMatch.Name);
return "must_match(tags, result, \n" +
" " + ToLua(order.First(), context, key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First(), context)).Indent().Indent() +
")";
}
var collectedList = new List<IExpression>();
var func = new List<IExpression>();
if (
UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare))
{
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(context);
var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$');
_deps.Add(funcName);
foreach (var expr in exprs)
{
var c = new List<IExpression>();
if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr))
{
luaExprs.Add(ToLua(c.First(), context, key));
continue;
}
if (expr.Types.First() is Curry curry
&& curry.ArgType.Equals(Typs.Tags))
{
var lua = ToLua(expr, context, key);
luaExprs.Add(lua);
}
}
return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) +
"\n })";
}
collectedMapping.Clear();
var dottedFunction = new List<IExpression>();
dottedFunction.Clear();
if (UnApply(
UnApply(
IsFunc(Funcs.Dot),
Assign(dottedFunction)
),
UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))).Invoke(bare)
)
{
var mapping = (Mapping) collectedMapping.First();
var baseFunc = (Function) dottedFunction.First();
_deps.Add(baseFunc.Name);
_deps.Add("table_to_list");
return baseFunc.Name +
"(table_to_list(tags, result, " +
("\n" + MappingToLua(mapping, context)).Indent().Indent() +
"))";
}
// The expression might be a function which still expects a string (the value from the tag) as argument
if (!(bare is Mapping) &&
bare.Types.First() is Curry curr &&
curr.ArgType.Equals(Typs.String))
{
var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\"")));
return ToLua(applied.Optimize(), context, key);
}
// The expression might consist of multiple nested functions
var fArgs = bare.DeconstructApply();
if (fArgs != null)
{
var (f, args) = fArgs.Value;
var baseFunc = (Function) f;
_deps.Add(baseFunc.Name);
return baseFunc.Name + "(" + string.Join(", ", args.Select(arg => ToLua(arg, context, key))) + ")";
}
var collected = new List<IExpression>();
switch (bare)
{
case FunctionCall fc:
var called = context.DefinedFunctions[fc.CalledFunctionName];
if (called.ProfileInternal)
{
return called.Name;
}
AddFunction(called, context);
return $"{fc.CalledFunctionName.FunctionName()}(parameters, tags, result)";
case Constant c:
return ConstantToLua(c, context);
case Mapping m:
return MappingToLua(m, context).Indent();
case Function f:
var fName = f.Name.TrimStart('$');
if (Funcs.Builtins.ContainsKey(fName))
{
_deps.Add(f.Name);
}
else
{
var definedFunc = context.DefinedFunctions[fName];
if (definedFunc.ProfileInternal)
{
return f.Name;
}
AddFunction(definedFunc, context);
}
return f.Name;
case Apply a when UnApply(IsFunc(Funcs.Const), Assign(collected)).Invoke(a):
return ToLua(collected.First(), context, key);
case Parameter p:
return $"parameters[\"{p.ParamName.FunctionName()}\"]";
default:
throw new Exception("Could not convert " + bare + " to a lua expression");
}
}
public static string ConstantToLua(Constant c, Context context)
{
var o = c.Evaluate(context);
switch (o)
{
case IExpression e:
return ConstantToLua(new Constant(e.Types.First(), e.Evaluate(null)), context);
case int i:
return "" + i;
case double d:
return "" + d;
case string s:
return '"' + s.Replace("\"", "\\\"") + '"';
case ValueTuple<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> ls:
var t = (c.Types.First() as ListType).InnerType;
return "{" + string.Join(", ", ls.Select(obj =>
{
var objInConstant = new Constant(t, obj);
if (obj is Constant asConstant)
{
objInConstant = asConstant;
}
return ConstantToLua(objInConstant, context);
})) + "}";
default:
return o.ToString();
}
}
public string MappingToLua(Mapping m, Context context)
{
var contents = m.StringToResultFunctions.Select(kv =>
{
var (key, expr) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return left + " = " + ToLua(expr, context, key);
}
);
return
"{\n " +
string.Join(",\n ", contents) +
"\n}";
}
private HashSet<string> addFunctions = new HashSet<string>();
public void AddFunction(AspectMetadata meta, Context context)
{
if (addFunctions.Contains(meta.Name))
{
// already added
return;
}
addFunctions.Add(meta.Name);
var possibleTags = meta.PossibleTags();
var usedParams = meta.UsedParameters();
var numberOfCombinations = possibleTags.Values.Select(lst => 1 + lst.Count).Multiply();
var impl = string.Join("\n",
"--[[",
meta.Description,
"",
"Unit: " + meta.Unit,
"Created by " + meta.Author,
"Originally defined in " + meta.Filepath,
"Uses tags: " + string.Join(", ", possibleTags.Keys),
"Used parameters: " + string.Join(", ", usedParams),
"Number of combintations: " + numberOfCombinations,
"Returns values: ",
"]]",
"function " + meta.Name.FunctionName() + "(parameters, tags, result)",
" return " + ToLua(meta.ExpressionImplementation, context),
"end"
);
_code.Add(impl);
foreach (var k in possibleTags.Keys)
{
_neededKeys.Add(k); // To generate a whitelist of OSM-keys that should be kept
}
}
private string CreateMembershipPreprocessor()
{
return "";
}
/// <summary>
/// Adds the necessary called functions and the profile main entry point
/// </summary>
public void CreateProfile(ProfileMetaData profile, Context context)
{
var defaultParameters = "\n";
foreach (var (name, (types, inFunction)) in profile.UsedParameters(context))
{
defaultParameters += $"{name}: {string.Join(", ", types)}\n" +
$" Used in {inFunction}\n";
}
var impl = string.Join("\n",
"",
"",
$"name = \"{profile.Name}\"",
"normalize = false",
"vehicle_type = {" + string.Join(", ", profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}",
"meta_whitelist = {" + string.Join(", ", profile.Metadata.Select(s => "\"" + s + "\"")) + "}",
"",
"",
"",
"--[[",
profile.Name,
"This is the main function called to calculate the access, oneway and speed.",
"Comfort is calculated as well, based on the parameters which are padded in",
"",
"Created by " + profile.Author,
"Originally defined in " + profile.Filename,
"Used parameters: " + defaultParameters.Indent(),
"]]",
"function " + profile.Name + "(parameters, tags, result)",
"",
" -- initialize the result table on the default values",
" result.access = 0",
" result.speed = 0",
" result.factor = 1",
" result.direction = 0",
" result.canstop = true",
" result.attributes_to_keep = {}",
"",
" local access = " + ToLua(profile.Access, context),
" if (access == nil or access == \"no\") then",
" return",
" end",
" local oneway = " + ToLua(profile.Oneway, context),
" local speed = " + ToLua(profile.Speed, context),
" local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m",
"");
impl +=
"\n local weight = \n ";
var weightParts = new List<string>();
foreach (var (parameterName, expression) in profile.Weights)
{
var weightPart = ToLua(new Parameter(parameterName), context) + " * ";
var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First());
if (subs != null && subs.TryGetValue("$a", out var resultType) &&
(resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String)))
{
weightPart += "parse(" + ToLua(expression, context) + ")";
}
else
{
weightPart += ToLua(expression, context);
}
weightParts.Add(weightPart);
}
impl += string.Join(" + \n ", weightParts);
impl += string.Join("\n",
"",
"",
"",
" -- put all the values into the result-table, as needed for itinero",
" result.access = 1",
" result.speed = speed",
" result.factor = 1/weight",
"",
" if (oneway == \"both\") then",
" result.oneway = 0",
" elseif (oneway == \"with\") then",
" result.oneway = 1",
" else",
" result.oneway = 2",
" end",
"",
"end",
"",
"",
"function default_parameters()",
" return " + profile.DefaultParameters.ToLuaTable(),
"end",
"",
""
);
impl += "\n\n" + CreateMembershipPreprocessor() + "\n\n";
var profiles = new List<string>();
foreach (var (name, subParams) in profile.Profiles)
{
var functionName = profile.Name + "_" + name;
subParams.TryGetValue("description", out var description);
profiles.Add(
string.Join(",\n ",
$" name = \"{name}\"",
" function_name = \"profile_" + functionName + "\"",
" metric = \"custom\""
)
);
impl += string.Join("\n",
"",
"--[[",
description,
"]]",
"function profile_" + functionName + "(tags, result)",
" local parameters = default_parameters()",
""
);
foreach (var (paramName, value) in subParams)
{
impl += $" parameters.{paramName.TrimStart('#').FunctionName()} = {value.Pretty()}\n";
}
impl += " " + profile.Name + "(parameters, tags, result)\n";
impl += "end\n";
}
impl += "\n\n\n";
impl += "profiles = {\n {\n" +
string.Join("\n },\n {\n ", profiles) + "\n }\n}";
_code.Add(impl);
}
public string ToLua()
{
var deps = _deps.ToList();
deps.Add("unitTest");
deps.Add("unitTestProfile");
deps.Add("inv");
deps.Add("double_compare");
deps.Sort();
var code = deps.Select(d => BasicFunctions[d]).ToList();
var keys = _neededKeys.Select(key => "\"" + key + "\"");
code.Add("\n\nprofile_whitelist = {" + string.Join(", ", keys) + "}");
code.AddRange(_code);
return string.Join("\n\n\n", code);
}
}
public static class StringExtensions
{
public static string ToLuaTable(this Dictionary<string, string> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = \"{value}\"";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string ToLuaTable(this Dictionary<string, object> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = {value.Pretty()}";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string FunctionName(this string s)
{
return s.Replace(" ", "_").Replace(".", "_").Replace("-", "_");
}
}
}

View file

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using AspectedRouting.Functions; using AspectedRouting.Language;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.IO namespace AspectedRouting.IO
{ {
@ -32,7 +32,7 @@ namespace AspectedRouting.IO
get get
{ {
var txt = "## Builtin functions\n\n"; var txt = "## Builtin functions\n\n";
foreach (var biFunc in Functions.Funcs.BuiltinNames) foreach (var biFunc in Funcs.BuiltinNames)
{ {
txt += "- " + biFunc + "\n"; txt += "- " + biFunc + "\n";
} }
@ -68,7 +68,7 @@ namespace AspectedRouting.IO
{ {
var args = f.ArgBreakdown(); var args = f.ArgBreakdown();
var header = "Argument name | "; var header = "Argument name | ";
var line = "--------------| - "; var line = "-------------- | - ";
for (int i = 0; i < f.Types.Count(); i++) for (int i = 0; i < f.Types.Count(); i++)
{ {
header += "| "; header += "| ";

View file

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Functions;
namespace AspectedRouting.IO
{
public struct Expected
{
public int Access, Oneway;
public double Speed, Weight;
public Expected(int access, int oneway, double speed, double weight)
{
Access = access;
Oneway = oneway;
Speed = speed;
Weight = weight;
if (Access == 0)
{
}
}
}
public class ProfileTestSuite
{
private readonly ProfileMetaData _profile;
private readonly string _profileName;
private readonly IEnumerable<(Expected, Dictionary<string, string> tags)> _tests;
public static ProfileTestSuite FromString(ProfileMetaData function, string profileName, string csvContents)
{
try
{
var all = csvContents.Split("\n").ToList();
var keys = all[0].Split(",").ToList();
keys = keys.GetRange(4, keys.Count - 4).Select(k => k.Trim()).ToList();
var tests = new List<(Expected, Dictionary<string, string>)>();
var line = 1;
foreach (var test in all.GetRange(1, all.Count - 1))
{
line++;
if (string.IsNullOrEmpty(test.Trim()))
{
continue;
}
try
{
var testData = test.Split(",").ToList();
var expected = new Expected(
int.Parse(testData[0]),
int.Parse(testData[1]),
double.Parse(testData[2]),
double.Parse(testData[3])
);
var vals = testData.GetRange(4, testData.Count - 4);
var tags = new Dictionary<string, string>();
for (int i = 0; i < keys.Count; i++)
{
if (i < vals.Count && !string.IsNullOrEmpty(vals[i]))
{
tags[keys[i]] = vals[i];
}
}
tests.Add((expected, tags));
}
catch (Exception e)
{
throw new Exception("On line " + line, e);
}
}
return new ProfileTestSuite(function, profileName, tests);
}
catch (Exception e)
{
throw new Exception("In the profile test file for " + profileName, e);
}
}
public ProfileTestSuite(
ProfileMetaData profile,
string profileName,
IEnumerable<(Expected, Dictionary<string, string> tags)> tests)
{
_profile = profile;
_profileName = profileName;
_tests = tests;
}
public string ToLua()
{
var tests = string.Join("\n",
_tests.Select((test, i) => ToLua(i, test.Item1, test.tags)));
return tests + "\n";
}
private string ToLua(int index, Expected expected, Dictionary<string, string> tags)
{
var parameters = new Dictionary<string, string>();
foreach (var (key, value) in tags)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
// function unit_test_profile(profile_function, profile_name, index, expected, tags)
return $"unit_test_profile(profile_bicycle_{_profileName.FunctionName()}, " +
$"\"{_profileName}\", " +
$"{index}, " +
$"{{access = {expected.Access}, speed = {expected.Speed}, oneway = {expected.Oneway}, weight = {expected.Weight} }}, " +
tags.ToLuaTable() +
")";
}
public void Run()
{
}
}
}

View file

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private readonly HashSet<string> _dependencies = new HashSet<string>();
private readonly HashSet<string> _neededKeys = new HashSet<string>();
private readonly List<string> _code = new List<string>();
private readonly List<string> _tests = new List<string>();
/// <summary>
/// A dictionary containing the implementation of basic functions
/// </summary>
/// <returns></returns>
private static IEnumerable<string> LoadFunctions(List<string> names)
{
var imps = new List<string>();
foreach (var name in names)
{
var path = $"IO/lua/{name}.lua";
if (File.Exists(path))
{
imps.Add(File.ReadAllText(path));
}
else
{
throw new FileNotFoundException(path);
}
}
return imps;
}
private void AddDep(string name)
{
_dependencies.Add(name);
}
public string ToLua()
{
var deps = _dependencies.ToList();
deps.Add("unitTestProfile");
deps.Add("inv");
deps.Add("double_compare");
var code = new List<string>();
code.Add($"-- Itinero 1.0-profile, generated on {DateTime.Now:s}");
code.Add("\n\n----------------------------- UTILS ---------------------------");
code.AddRange(LoadFunctions(deps).ToList());
code.Add("\n\n----------------------------- PROFILE ---------------------------");
var keys = _neededKeys.Select(key => "\"" + key + "\"");
code.Add("\n\nprofile_whitelist = {" + string.Join(", ", keys) + "}");
code.AddRange(_code);
code.Add("\n\n ------------------------------- TESTS -------------------------");
code.AddRange(_tests);
var compatibility = string.Join("\n",
"",
"if (itinero == nil) then",
" itinero = {}",
" itinero.log = print",
"",
" -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended",
" runTests = true",
"",
"",
"else",
" print = itinero.log",
"end",
"",
"if (not failed_tests and not failed_profile_tests) then",
" print(\"Tests OK\")",
"end"
);
code.Add(compatibility);
return string.Join("\n\n\n", code);
}
}
}

View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace AspectedRouting.IO.itinero1
{
public static class LuaStringExtensions
{
public static string ToLuaTable(this Dictionary<string, string> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = \"{value}\"";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string FunctionName(this string s)
{
return s.Replace("$", "")
.Replace("#", "")
.Replace(" ", "_")
.Replace(".", "_")
.Replace("-", "_");
}
}
}

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private readonly HashSet<string> _alreadyAddedFunctions = new HashSet<string>();
public void AddFunction(AspectMetadata meta)
{
if (_alreadyAddedFunctions.Contains(meta.Name))
{
// already added
return;
}
_alreadyAddedFunctions.Add(meta.Name);
var possibleTags = meta.PossibleTags() ?? new Dictionary<string, List<string>>();
var numberOfCombinations = 1;
numberOfCombinations = possibleTags.Values.Select(lst => 1 + lst.Count).Multiply();
var usedParams = meta.UsedParameters();
var funcNameDeclaration = "";
meta.Visit(e =>
{
if (e is Function f && f.Name.Equals(Funcs.MemberOf.Name))
{
funcNameDeclaration = $"\n local funcName = \"{meta.Name.FunctionName()}\"";
}
return true;
});
var impl = string.Join("\n",
"--[[",
meta.Description,
"",
"Unit: " + meta.Unit,
"Created by " + meta.Author,
"Originally defined in " + meta.Filepath,
"Uses tags: " + string.Join(", ", possibleTags.Keys),
"Used parameters: " + string.Join(", ", usedParams),
"Number of combintations: " + numberOfCombinations,
"Returns values: ",
"]]",
"function " + meta.Name.FunctionName() + "(parameters, tags, result)" + funcNameDeclaration,
" return " + ToLua(meta.ExpressionImplementation),
"end"
);
_code.Add(impl);
foreach (var k in possibleTags.Keys)
{
_neededKeys.Add(k); // To generate a whitelist of OSM-keys that should be kept
}
}
}
}

View file

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using static AspectedRouting.Language.Deconstruct;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private string ToLua(IExpression bare, string key = "nil")
{
var collectedMapping = new List<IExpression>();
var order = new List<IExpression>();
if (UnApply(
UnApply(
IsFunc(Funcs.FirstOf),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
AddDep(Funcs.FirstOf.Name);
return "first_match_of(tags, result, \n" +
" " + ToLua(order.First(), key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First())).Indent().Indent() +
")";
}
if (UnApply(
UnApply(
IsFunc(Funcs.MustMatch),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
AddDep(Funcs.MustMatch.Name);
return "must_match(tags, result, \n" +
" " + ToLua(order.First(), key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First())).Indent().Indent() +
")";
}
if (UnApply(
IsFunc(Funcs.MemberOf),
Any
).Invoke(bare))
{
AddDep("memberOf");
return "member_of(funcName, parameters, tags, result)";
}
var collectedList = new List<IExpression>();
var func = new List<IExpression>();
if (
UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare))
{
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(_context);
var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$');
AddDep(funcName);
foreach (var expr in exprs)
{
var c = new List<IExpression>();
if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr))
{
luaExprs.Add(ToLua(c.First(), key));
continue;
}
if (expr.Types.First() is Curry curry
&& curry.ArgType.Equals(Typs.Tags))
{
var lua = ToLua(expr, key);
luaExprs.Add(lua);
}
}
return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) +
"\n })";
}
collectedMapping.Clear();
var dottedFunction = new List<IExpression>();
dottedFunction.Clear();
if (UnApply(
UnApply(
IsFunc(Funcs.Dot),
Assign(dottedFunction)
),
UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))).Invoke(bare)
)
{
var mapping = (Mapping) collectedMapping.First();
var baseFunc = (Function) dottedFunction.First();
AddDep(baseFunc.Name);
AddDep("table_to_list");
return baseFunc.Name +
"(table_to_list(tags, result, " +
("\n" + MappingToLua(mapping)).Indent().Indent() +
"))";
}
// The expression might be a function which still expects a string (the value from the tag) as argument
if (!(bare is Mapping) &&
bare.Types.First() is Curry curr &&
curr.ArgType.Equals(Typs.String))
{
var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\"")));
return ToLua(applied.Optimize(), key);
}
// The expression might consist of multiple nested functions
var fArgs = bare.DeconstructApply();
if (fArgs != null)
{
var (f, args) = fArgs.Value;
var baseFunc = (Function) f;
AddDep(baseFunc.Name);
return baseFunc.Name + "(" + string.Join(", ", args.Select(arg => ToLua(arg, key))) + ")";
}
var collected = new List<IExpression>();
switch (bare)
{
case FunctionCall fc:
var called = _context.DefinedFunctions[fc.CalledFunctionName];
if (called.ProfileInternal)
{
return called.Name;
}
AddFunction(called);
return $"{fc.CalledFunctionName.FunctionName()}(parameters, tags, result)";
case Constant c:
return ConstantToLua(c);
case Mapping m:
return MappingToLua(m).Indent();
case Function f:
var fName = f.Name.TrimStart('$');
if (Funcs.Builtins.ContainsKey(fName))
{
AddDep(f.Name);
}
else
{
var definedFunc = _context.DefinedFunctions[fName];
if (definedFunc.ProfileInternal)
{
return f.Name;
}
AddFunction(definedFunc);
}
return f.Name;
case Apply a when UnApply(IsFunc(Funcs.Const), Assign(collected))
.Invoke(a):
return ToLua(collected.First(), key);
case Parameter p:
return $"parameters[\"{p.ParamName.FunctionName()}\"]";
default:
throw new Exception("Could not convert " + bare + " to a lua expression");
}
}
public string MappingToLua(Mapping m)
{
var contents = m.StringToResultFunctions.Select(kv =>
{
var (key, expr) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return left + " = " + ToLua(expr, key);
}
);
return
"{\n " +
string.Join(",\n ", contents) +
"\n}";
}
/// <summary>
/// Neatly creates a value expression in lua, based on a constant
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
private string ConstantToLua(Constant c)
{
var o = c.Evaluate(_context);
switch (o)
{
case IExpression e:
return ConstantToLua(new Constant(e.Types.First(), e.Evaluate(null)));
case int i:
return "" + i;
case double d:
return "" + d;
case string s:
return '"' + s.Replace("\"", "\\\"") + '"';
case ValueTuple<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> ls:
var t = (c.Types.First() as ListType).InnerType;
return "{" + string.Join(", ", ls.Select(obj =>
{
var objInConstant = new Constant(t, obj);
if (obj is Constant asConstant)
{
objInConstant = asConstant;
}
return ConstantToLua(objInConstant);
})) + "}";
default:
return o.ToString();
}
}
}
}

View file

@ -0,0 +1,256 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
private readonly Context _context;
public LuaPrinter(Context context)
{
_context = context;
}
private void CreateMembershipPreprocessor(ProfileMetaData profile)
{
var memberships = Analysis.MembershipMappingsFor(profile, _context);
foreach (var (calledInFunction, membership) in memberships)
{
var funcMetaData = new AspectMetadata(
membership,
"relation_preprocessing_for_" + calledInFunction.FunctionName(),
"Function preprocessing needed for aspect " + calledInFunction +
", called by the relation preprocessor",
"Generator", "", "NA"
);
AddFunction(funcMetaData);
}
var func = new List<string>
{
"",
"",
"-- Processes the relation. All tags which are added to result.attributes_to_keep will be copied to 'attributes' of each individual way",
"function relation_tag_processor(relation_tags, result)",
" local parameters = {}",
" local subresult = {}",
" local matched = false",
" result.attributes_to_keep = {}"
};
foreach (var (calledInFunction, _) in memberships)
{
foreach (var (behaviourName, parameters) in profile.Behaviours)
{
var preProcName = "relation_preprocessing_for_" + calledInFunction.FunctionName();
func.Add("");
func.Add("");
func.Add(" subresult.attributes_to_keep = {}");
func.Add(" parameters = default_parameters()");
func.Add(ParametersToLua(parameters));
func.Add($" matched = {preProcName}(parameters, relation_tags, subresult)");
func.Add(" if (matched) then");
var tagKey = "_relation:" + behaviourName.FunctionName() + ":" + calledInFunction.FunctionName();
_neededKeys.Add("_relation:"+calledInFunction.FunctionName()); // Slightly different then tagkey!
func.Add($" result.attributes_to_keep[\"{tagKey}\"] = \"yes\"");
func.Add(" end");
}
}
func.Add("end");
_code.Add(string.Join("\n", func));
}
/// <summary>
/// Adds the necessary called functions and the profile main entry point
/// </summary>
public void AddProfile(ProfileMetaData profile)
{
var defaultParameters = "\n";
foreach (var (name, (types, inFunction)) in profile.UsedParameters(_context))
{
defaultParameters += $"{name}: {string.Join(", ", types)}\n" +
$" Used in {inFunction}\n";
}
CreateMembershipPreprocessor(profile);
var impl = string.Join("\n",
"",
"",
$"name = \"{profile.Name}\"",
"normalize = false",
"vehicle_type = {" + string.Join(", ", profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}",
"meta_whitelist = {" + string.Join(", ", profile.Metadata.Select(s => "\"" + s + "\"")) + "}",
"",
"",
"",
"--[[",
profile.Name,
"This is the main function called to calculate the access, oneway and speed.",
"Comfort is calculated as well, based on the parameters which are padded in",
"",
"Created by " + profile.Author,
"Originally defined in " + profile.Filename,
"Used parameters: " + defaultParameters.Indent(),
"]]",
"function " + profile.Name + "(parameters, tags, result)",
"",
" -- initialize the result table on the default values",
" result.access = 0",
" result.speed = 0",
" result.factor = 1",
" result.direction = 0",
" result.canstop = true",
" result.attributes_to_keep = {}",
"",
" local access = " + ToLua(profile.Access),
" if (access == nil or access == \"no\") then",
" return",
" end",
" tags.access = access",
" local oneway = " + ToLua(profile.Oneway),
" tags.oneway = oneway",
" local speed = " + ToLua(profile.Speed),
" tags.speed = speed",
" local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m",
"");
impl +=
"\n local priority = \n ";
var weightParts = new List<string>();
foreach (var (parameterName, expression) in profile.Priority)
{
var priorityPart = ToLua(new Parameter(parameterName)) + " * ";
var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First());
if (subs != null && subs.TryGetValue("$a", out var resultType) &&
(resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String)))
{
priorityPart += "parse(" + ToLua(expression) + ")";
}
else
{
priorityPart += ToLua(expression);
}
weightParts.Add(priorityPart);
}
impl += string.Join(" + \n ", weightParts);
impl += string.Join("\n",
"",
"",
"",
" -- put all the values into the result-table, as needed for itinero",
" result.access = 1",
" result.speed = speed",
" result.factor = priority",
"",
" if (oneway == \"both\") then",
" result.oneway = 0",
" elseif (oneway == \"with\") then",
" result.oneway = 1",
" else",
" result.oneway = 2",
" end",
"",
"end",
"",
"",
"function default_parameters()",
" local parameters = {}",
ParametersToLua(profile.DefaultParameters),
" return parameters",
"end",
"",
""
);
var profiles = new List<string>();
foreach (var (name, subParams) in profile.Behaviours)
{
impl += BehaviourFunction(profile, name, subParams, profiles);
}
impl += "\n\n\n";
impl += "profiles = {\n {\n" +
string.Join("\n },\n {\n ", profiles) + "\n }\n}";
_code.Add(impl);
}
private string BehaviourFunction(ProfileMetaData profile,
string name,
Dictionary<string, IExpression> subParams, List<string> profiles)
{
var functionName = profile.Name + "_" + name;
subParams.TryGetValue("description", out var description);
profiles.Add(
string.Join(",\n ",
$" name = \"{name}\"",
" function_name = \"profile_" + functionName + "\"",
" metric = \"custom\""
)
);
AddDep("remove_relation_prefix");
var impl = string.Join("\n",
"",
"--[[",
description,
"]]",
"function profile_" + functionName + "(tags, result)",
$" tags = remove_relation_prefix(tags, \"{name.FunctionName()}\")",
" local parameters = default_parameters()",
""
);
impl += ParametersToLua(subParams);
impl += " " + profile.Name + "(parameters, tags, result)\n";
impl += "end\n";
return impl;
}
/// <summary>
/// `local parameters = default_parameters()` must still be invoked by caller!
/// </summary>
/// <param name="subParams"></param>
/// <returns></returns>
private string ParametersToLua(Dictionary<string, IExpression> subParams)
{
var impl = "";
foreach (var (paramName, value) in subParams)
{
if (paramName.Equals("description"))
{
continue;
}
impl += $" parameters.{paramName.TrimStart('#').FunctionName()} = {ToLua(value)}\n";
}
return impl;
}
}
}

View file

@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Tests;
namespace AspectedRouting.IO.itinero1
{
public partial class LuaPrinter
{
public void AddTestSuite(ProfileTestSuite testSuite)
{
var tests = string.Join("\n",
testSuite.Tests.Select((test, i) => ToLua(testSuite, i, test.Item1, test.tags)));
_tests.Add(tests);
}
private string ToLua(ProfileTestSuite testSuite, int index, Expected expected, Dictionary<string, string> tags)
{
AddDep("debug_table");
var parameters = new Dictionary<string, string>();
var keysToCheck = new List<string>();
foreach (var (key, value) in tags)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
if(key.StartsWith("_relation:"))
{
keysToCheck.Add(key);
}
}
foreach (var key in keysToCheck)
{
var newKey = key.Replace(".", "_");
tags[newKey] = tags[key];
tags.Remove(key);
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
// function unit_test_profile(profile_function, profile_name, index, expected, tags)
return $"unit_test_profile(profile_bicycle_{testSuite.BehaviourName.FunctionName()}, " +
$"\"{testSuite.BehaviourName}\", " +
$"{index}, " +
$"{{access = \"{D(expected.Access)}\", speed = {expected.Speed}, oneway = \"{D(expected.Oneway)}\", weight = {expected.Weight} }}, " +
tags.ToLuaTable() +
")";
}
private string D(string s)
{
if (string.IsNullOrEmpty(s))
{
return "0";
}
return s;
}
public void AddTestSuite(FunctionTestSuite testSuite)
{
var fName = testSuite.FunctionToApply.Name;
var tests = string.Join("\n",
testSuite.Tests.Select((test, i) => ToLua(fName, i, test.expected, test.tags)));
_tests.Add(tests);
}
private string ToLua(string functionToApplyName, int index, string expected, Dictionary<string, string> tags)
{
var parameters = new Dictionary<string, string>();
foreach (var (key, value) in tags)
{
if (key.StartsWith("#"))
{
parameters[key.TrimStart('#')] = value;
}
}
foreach (var (paramName, _) in parameters)
{
tags.Remove("#" + paramName);
}
AddDep("unitTest");
var funcName = functionToApplyName.Replace(" ", "_").Replace(".", "_");
return
$"unit_test({funcName}, \"{functionToApplyName}\", {index}, \"{expected}\", {parameters.ToLuaTable()}, {tags.ToLuaTable()})";
}
}
}

View file

@ -1,512 +1,463 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using AspectedRouting.Functions; using AspectedRouting.Language;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using static AspectedRouting.Functions.Funcs; using AspectedRouting.Language.Functions;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.IO
{ namespace AspectedRouting.IO.jsonParser
public static class JsonParser {
{ public static partial class JsonParser
public static AspectMetadata AspectFromJson(Context c, string json, string fileName) {
{ public static AspectMetadata AspectFromJson(Context c, string json, string fileName)
try {
{ try
var doc = JsonDocument.Parse(json); {
if (doc.RootElement.TryGetProperty("defaults", out _)) var doc = JsonDocument.Parse(json);
{ if (doc.RootElement.TryGetProperty("defaults", out _))
// this is a profile {
return null; // this is a profile
} return null;
}
return doc.RootElement.ParseAspect(fileName, c);
} return doc.RootElement.ParseAspect(fileName, c);
catch (Exception e) }
{ catch (Exception e)
throw new Exception("In the file " + fileName, e); {
} throw new Exception("In the file " + fileName, e);
} }
}
public static ProfileMetaData ProfileFromJson(Context c, string json, FileInfo f)
{ public static ProfileMetaData ProfileFromJson(Context c, string json, FileInfo f)
try {
{ try
var doc = JsonDocument.Parse(json); {
if (!doc.RootElement.TryGetProperty("defaults", out _)) var doc = JsonDocument.Parse(json);
{ if (!doc.RootElement.TryGetProperty("defaults", out _))
return null; {
// this is an aspect return null;
} // this is an aspect
}
return ParseProfile(doc.RootElement, c, f);
} return ParseProfile(doc.RootElement, c, f);
catch (Exception e) }
{ catch (Exception e)
throw new Exception("In the file " + f, e); {
} throw new Exception("In the file " + f, e);
} }
}
private static readonly IExpression _mconst = private static ProfileMetaData ParseProfile(this JsonElement e, Context context, FileInfo filepath)
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.Const); {
var name = e.Get("name");
private static readonly IExpression _mappingWrapper = var author = e.TryGet("author");
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), StringStringToTags); if (filepath != null && !(name + ".json").ToLower().Equals(filepath.Name.ToLower()))
{
private static IExpression ParseMapping(IEnumerable<JsonProperty> allArgs, Context context) throw new ArgumentException($"Filename does not match the defined name: " +
{ $"filename is {filepath.Name}, declared name is {name}");
var keys = new List<string>(); }
var exprs = new List<IExpression>();
var vehicleTypes = e.GetProperty("vehicletypes").EnumerateArray().Select(
foreach (var prop in allArgs) el => el.GetString()).ToList();
{ var metadata = e.GetProperty("metadata").EnumerateArray().Select(
if (prop.Name.Equals("#")) el => el.GetString()).ToList();
{
continue;
} var access = ParseProfileProperty(e, context, "access").Finalize();
var oneway = ParseProfileProperty(e, context, "oneway").Finalize();
keys.Add(prop.Name); var speed = ParseProfileProperty(e, context, "speed").Finalize();
var argExpr = ParseExpression(prop.Value, context);
IExpression mappingWithOptArg;
if (argExpr.Types.Count() == 1 && argExpr.Types.First().Equals(Typs.String)) IExpression TagsApplied(IExpression x)
{ {
mappingWithOptArg = return new Apply(x, new Constant(new Dictionary<string, string>()));
Either(Funcs.Id, Funcs.Eq, argExpr); }
}
else context.AddFunction("speed",
{ new AspectMetadata(TagsApplied(speed), "speed", "The speed of this profile", author, "", filepath.Name,
mappingWithOptArg = new Apply(_mconst, argExpr); true));
} context.AddFunction("access",
new AspectMetadata(TagsApplied(access), "access", "The access of this profile", author, "",
exprs.Add(mappingWithOptArg); filepath.Name,
} true));
context.AddFunction("oneway",
try new AspectMetadata(TagsApplied(oneway), "oneway", "The oneway of this profile", author, "",
{ filepath.Name,
var simpleMapping = new Mapping(keys, exprs); true));
return new Apply(_mappingWrapper, simpleMapping); context.AddFunction("distance",
} new AspectMetadata(new Constant(1), "distance", "The distance travelled of this profile", author, "",
catch (Exception e) filepath.Name,
{ true));
throw new Exception("While constructing a mapping with members " + string.Join(", ", exprs) +
": " + e.Message, e);
} var weights = new Dictionary<string, IExpression>();
} var weightProperty = e.GetProperty("priority");
foreach (var prop in weightProperty.EnumerateObject())
private static IExpression ParseExpression(this JsonElement e, Context context) {
{ var parameter = prop.Name.TrimStart('#');
if (e.ValueKind == JsonValueKind.Object) var factor = ParseExpression(prop.Value, context).Finalize();
{ weights[parameter] = factor;
// Parse an actual function }
var funcCall = e.EnumerateObject().Where(v => v.Name.StartsWith("$")).ToList();
var allArgs = e.EnumerateObject().Where(v => !v.Name.StartsWith("$")).ToList(); var profiles = new Dictionary<string, Dictionary<string, IExpression>>();
if (funcCall.Count > 2) foreach (var profile in e.GetProperty("behaviours").EnumerateObject())
{ {
throw new ArgumentException("Multiple calls defined in object " + e); profiles[profile.Name] = ParseParameters(profile.Value);
} }
if (funcCall.Count == 1) return new ProfileMetaData(
{ name,
return ParseFunctionCall(context, funcCall, allArgs); e.Get("description"),
} author,
filepath?.DirectoryName ?? "unknown",
// Funccall has no elements: this is a mapping of strings or tags onto a value vehicleTypes,
e.GetProperty("defaults").ParseParameters(),
return ParseMapping(allArgs, context); profiles,
} access,
oneway,
if (e.ValueKind == JsonValueKind.Array) speed,
{ weights,
var exprs = e.EnumerateArray().Select(json => metadata
Either(Funcs.Id, Funcs.Const, json.ParseExpression(context))); );
var list = new Constant(exprs); }
return Either(Funcs.Id, Funcs.ListDot, list);
} private static readonly IExpression _mconst =
new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.Const);
if (e.ValueKind == JsonValueKind.Number)
{ private static readonly IExpression _mappingWrapper =
if (e.TryGetDouble(out var d)) new Apply(new Apply(Funcs.EitherFunc, Funcs.Id), Funcs.StringStringToTags);
{
return new Constant(d); private static IExpression ParseMapping(IEnumerable<JsonProperty> allArgs, Context context)
} {
var keys = new List<string>();
if (e.TryGetInt32(out var i)) var exprs = new List<IExpression>();
{
return new Constant(i); foreach (var prop in allArgs)
} {
} if (prop.Name.Equals("#"))
{
if (e.ValueKind == JsonValueKind.True) continue;
{ }
return new Constant(Typs.Bool, "yes");
} keys.Add(prop.Name);
var argExpr = ParseExpression(prop.Value, context);
if (e.ValueKind == JsonValueKind.False) IExpression mappingWithOptArg;
{ if (argExpr.Types.Count() == 1 && argExpr.Types.First().Equals(Typs.String))
return new Constant(Typs.Bool, "no"); {
} mappingWithOptArg =
Funcs.Either(Funcs.Id, Funcs.Eq, argExpr);
if (e.ValueKind == JsonValueKind.String) }
{ else
var s = e.GetString(); {
if (s.StartsWith("$")) mappingWithOptArg = new Apply(_mconst, argExpr);
{ }
var bi = BuiltinByName(s);
exprs.Add(mappingWithOptArg);
if (bi != null) }
{
return Either(Funcs.Dot, Funcs.Id, bi); try
} {
var simpleMapping = new Mapping(keys, exprs);
var definedFunc = context.GetFunction(s); return new Apply(_mappingWrapper, simpleMapping);
return Either(Funcs.Dot, Funcs.Id, new FunctionCall(s, definedFunc.Types)); }
} catch (Exception e)
{
if (s.StartsWith("#")) throw new Exception("While constructing a mapping with members " + string.Join(", ", exprs) +
{ ": " + e.Message, e);
// This is a parameter, the type of it is free }
return new Parameter(s); }
}
private static IExpression ParseExpression(this JsonElement e, Context context)
return new Constant(s); {
} if (e.ValueKind == JsonValueKind.Object)
{
// Parse an actual function
throw new Exception("Could not parse " + e); var funcCall = e.EnumerateObject().Where(v => v.Name.StartsWith("$")).ToList();
} var allArgs = e.EnumerateObject().Where(v => !v.Name.StartsWith("$")).ToList();
private static IExpression ParseFunctionCall(Context context, IReadOnlyCollection<JsonProperty> funcCall, if (funcCall.Count > 2)
IEnumerable<JsonProperty> allArgs) {
{ throw new ArgumentException("Multiple calls defined in object " + e);
var funcName = funcCall.First().Name; }
var func = BuiltinByName(funcName); if (funcCall.Count == 1)
{
// The list where all the arguments are collected return ParseFunctionCall(context, funcCall, allArgs);
var args = new List<IExpression>(); }
// Funccall has no elements: this is a mapping of strings or tags onto a value
// First argument of the function is the value of this property, e.g.
// { "$f": "xxx", "a2":"yyy", "a3":"zzz" } return ParseMapping(allArgs, context);
var firstArgument = ParseExpression(funcCall.First().Value, context); }
if (e.ValueKind == JsonValueKind.Array)
// Cheat for the very special case 'mustMatch' {
if (func.Equals(Funcs.MustMatch)) var exprs = e.EnumerateArray().Select(json =>
{ Funcs.Either(Funcs.Id, Funcs.Const, json.ParseExpression(context)));
// It gets an extra argument injected var list = new Constant(exprs);
var neededKeys = firstArgument.PossibleTags().Keys.ToList(); return Funcs.Either(Funcs.Id, Funcs.ListDot, list);
var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys); }
args.Add(neededKeysArg);
args.Add(firstArgument); if (e.ValueKind == JsonValueKind.Number)
return func.Apply(args); {
} if (e.TryGetDouble(out var d))
{
args.Add(firstArgument); return new Constant(d);
}
var allExprs = allArgs
.Where(kv => !kv.NameEquals("#")) // Leave out comments if (e.TryGetInt32(out var i))
.ToDictionary(kv => kv.Name, kv => kv.Value.ParseExpression(context)); {
return new Constant(i);
}
if (allExprs.Count > 1) }
{
if (func.ArgNames == null || func.ArgNames.Count < 2) if (e.ValueKind == JsonValueKind.True)
throw new ArgumentException("{funcName} does not specify argument names"); {
return new Constant(Typs.Bool, "yes");
foreach (var argName in func.ArgNames) }
{
args.Add(allExprs[argName]); if (e.ValueKind == JsonValueKind.False)
} {
} return new Constant(Typs.Bool, "no");
else if (allExprs.Count == 1) }
{
args.Add(allExprs.Single().Value); if (e.ValueKind == JsonValueKind.String)
} {
var s = e.GetString();
return Either(Funcs.Id, Funcs.Dot, func).Apply(args); if (s.StartsWith("$"))
} {
var bi = Funcs.BuiltinByName(s);
private static IExpression GetTopLevelExpression(this JsonElement root, Context context)
{ if (bi != null)
IExpression mapping = null; {
if (root.TryGetProperty("value", out var j)) return Funcs.Either(Funcs.Dot, Funcs.Id, bi);
{ }
// The expression is placed in the default 'value' location
mapping = j.ParseExpression(context); var definedFunc = context.GetFunction(s);
} return Funcs.Either(Funcs.Dot, Funcs.Id, new FunctionCall(s, definedFunc.Types));
}
// We search for the function call with '$' if (s.StartsWith("#"))
foreach (var prop in root.EnumerateObject()) {
{ // This is a parameter, the type of it is free
if (!prop.Name.StartsWith("$")) continue; return new Parameter(s);
}
var f = (IExpression) BuiltinByName(prop.Name); return new Constant(s);
if (f == null) }
{
throw new KeyNotFoundException("The builtin function " + f + " was not found");
} throw new Exception("Could not parse " + e);
}
var fArg = prop.Value.ParseExpression(context);
private static IExpression ParseFunctionCall(Context context, IReadOnlyCollection<JsonProperty> funcCall,
if (fArg == null) IEnumerable<JsonProperty> allArgs)
{ {
throw new ArgumentException("Could not type expression " + prop); var funcName = funcCall.First().Name;
}
var func = Funcs.BuiltinByName(funcName);
if (mapping != null) if (func == null)
{ {
// This is probably a firstOrderedVersion, a default, or some other function that should be applied throw new ArgumentException($"The function with name {funcName} is not found");
return }
new Apply(
Either(Funcs.Id, Funcs.Dot, new Apply(f, fArg)), mapping // The list where all the arguments are collected
); var args = new List<IExpression>();
}
// Cheat for the very special case 'mustMatch' // First argument of the function is the value of this property, e.g.
if (f.Equals(Funcs.MustMatch)) // { "$f": "xxx", "a2":"yyy", "a3":"zzz" }
{ var firstArgument = ParseExpression(funcCall.First().Value, context);
// It gets an extra argument injected
var neededKeys = fArg.PossibleTags().Keys.ToList();
var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys); // Cheat for the very special case 'mustMatch'
f = f.Apply(new[] {neededKeysArg}); if (func.Equals(Funcs.MustMatch))
} {
// It gets an extra argument injected
var appliedDot = new Apply(new Apply(Funcs.Dot, f), fArg); var neededKeys = firstArgument.PossibleTags().Keys.ToList();
var appliedDirect = new Apply(f, fArg); var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys);
args.Add(neededKeysArg);
if (!appliedDot.Types.Any()) args.Add(firstArgument);
{ return func.Apply(args);
// Applied dot doesn't work out, so we return the other one }
return appliedDirect;
} args.Add(firstArgument);
if (!appliedDirect.Types.Any()) var allExprs = allArgs
{ .Where(kv => !kv.NameEquals("#")) // Leave out comments
return appliedDot; .ToDictionary(kv => kv.Name, kv => kv.Value.ParseExpression(context));
}
var eithered = new Apply(new Apply(Funcs.EitherFunc, appliedDot), appliedDirect); if (allExprs.Count > 1)
{
if (func.ArgNames == null || func.ArgNames.Count < 2)
// We apply the builtin function through a dot throw new ArgumentException("{funcName} does not specify argument names");
return eithered;
} foreach (var argName in func.ArgNames)
{
args.Add(allExprs[argName]);
throw new Exception( }
"No top level reducer found. Did you forget the '$' in the reducing function? Did your forget 'value' to add the mapping?"); }
} else if (allExprs.Count == 1)
{
private static IExpression ParseProfileProperty(JsonElement e, Context c, string property) args.Add(allExprs.Single().Value);
{ }
try
{ return Funcs.Either(Funcs.Id, Funcs.Dot, func).Apply(args);
var prop = e.GetProperty(property); }
return ParseExpression(prop, c)
.Specialize(new Curry(Typs.Tags, new Var("a")))
.Optimize(); private static IExpression GetTopLevelExpression(this JsonElement root, Context context)
} {
catch (Exception exc) IExpression mapping = null;
{ if (root.TryGetProperty("value", out var j))
throw new Exception("While parsing the property " + property, exc); {
} // The expression is placed in the default 'value' location
} mapping = j.ParseExpression(context);
}
private static Dictionary<string, object> ParseParameters(this JsonElement e)
{
var ps = new Dictionary<string, object>(); // We search for the function call with '$'
foreach (var obj in e.EnumerateObject()) foreach (var prop in root.EnumerateObject())
{ {
var nm = obj.Name.TrimStart('#'); if (!prop.Name.StartsWith("$")) continue;
switch (obj.Value.ValueKind)
{
case JsonValueKind.String: var f = (IExpression) Funcs.BuiltinByName(prop.Name);
ps[nm] = obj.Value.ToString(); if (f == null)
break; {
case JsonValueKind.Number: throw new KeyNotFoundException($"The builtin function {prop.Name} was not found");
ps[nm] = obj.Value.GetDouble(); }
break;
case JsonValueKind.True: var fArg = prop.Value.ParseExpression(context);
ps[nm] = "yes";
break; if (fArg == null)
case JsonValueKind.False: {
ps[nm] = "no"; throw new ArgumentException("Could not type expression " + prop);
break; }
default:
throw new ArgumentOutOfRangeException();
} if (mapping != null)
} {
// This is probably a firstOrderedVersion, a default, or some other function that should be applied
return ps; return
} new Apply(
Funcs.Either(Funcs.Id, Funcs.Dot, new Apply(f, fArg)), mapping
public static ProfileMetaData ParseProfile(this JsonElement e, Context context, FileInfo filepath) );
{ }
var name = e.Get("name");
var author = e.TryGet("author"); // Cheat for the very special case 'mustMatch'
if (filepath != null && !(name + ".json").ToLower().Equals(filepath.Name.ToLower())) if (f.Equals(Funcs.MustMatch))
{ {
throw new ArgumentException($"Filename does not match the defined name: " + // It gets an extra argument injected
$"filename is {filepath.Name}, declared name is {name}"); var neededKeys = fArg.PossibleTags().Keys.ToList();
} var neededKeysArg = new Constant(new ListType(Typs.String), neededKeys);
f = f.Apply(new[] {neededKeysArg});
var vehicleTypes = e.GetProperty("vehicletypes").EnumerateArray().Select( }
el => el.GetString()).ToList();
var metadata = e.GetProperty("metadata").EnumerateArray().Select( var appliedDot = new Apply(new Apply(Funcs.Dot, f), fArg);
el => el.GetString()).ToList(); var appliedDirect = new Apply(f, fArg);
if (!appliedDot.Types.Any())
var access = ParseProfileProperty(e, context, "access").Finalize(); {
var oneway = ParseProfileProperty(e, context, "oneway").Finalize(); // Applied dot doesn't work out, so we return the other one
var speed = ParseProfileProperty(e, context, "speed").Finalize(); return appliedDirect;
}
IExpression TagsApplied(IExpression x) if (!appliedDirect.Types.Any())
{ {
return new Apply(x, new Constant(new Dictionary<string, string>())); return appliedDot;
} }
context.AddFunction("speed", var eithered = new Apply(new Apply(Funcs.EitherFunc, appliedDot), appliedDirect);
new AspectMetadata(TagsApplied(speed), "speed", "The speed of this profile", author, "", filepath.Name,
true));
context.AddFunction("access", // We apply the builtin function through a dot
new AspectMetadata(TagsApplied(access), "access", "The access of this profile", author, "", return eithered;
filepath.Name, }
true));
context.AddFunction("oneway",
new AspectMetadata(TagsApplied(oneway), "oneway", "The oneway of this profile", author, "", throw new Exception(
filepath.Name, "No top level reducer found. Did you forget the '$' in the reducing function? Did your forget 'value' to add the mapping?");
true)); }
context.AddFunction("distance",
new AspectMetadata(new Constant(1), "distance", "The distance travelled of this profile", author, "", private static AspectMetadata ParseAspect(this JsonElement e, string filepath, Context context)
filepath.Name, {
true)); var expr = GetTopLevelExpression(e, context);
var weights = new Dictionary<string, IExpression>(); var targetTypes = new List<Type>();
var weightProperty = e.GetProperty("weight"); foreach (var t in expr.Types)
foreach (var prop in weightProperty.EnumerateObject()) {
{ var a = Var.Fresh(t);
var parameter = prop.Name.TrimStart('#'); var b = Var.Fresh(new Curry(a, t));
var factor = ParseExpression(prop.Value, context).Finalize();
weights[parameter] = factor; if (t.Unify(new Curry(Typs.Tags, a)) != null &&
} t.Unify(new Curry(Typs.Tags, new Curry(a, b))) == null
) // Second should not match
var profiles = new Dictionary<string, Dictionary<string, object>>(); {
// The target type is 'Tags -> a', where a is NOT a curry
foreach (var profile in e.GetProperty("profiles").EnumerateObject()) targetTypes.Add(t);
{ }
profiles[profile.Name] = ParseParameters(profile.Value); }
}
if (targetTypes.Count == 0)
return new ProfileMetaData( {
name, throw new ArgumentException("The top level expression has types:\n" +
e.Get("description"), string.Join("\n ", expr.Types) +
author, "\nwhich can not be specialized into a form suiting `tags -> a`\n" + expr);
filepath?.DirectoryName ?? "unknown", }
vehicleTypes,
e.GetProperty("defaults").ParseParameters(), var exprSpec = expr.Specialize(targetTypes);
profiles, if (expr == null)
access, {
oneway, throw new Exception("Could not specialize the expression " + expr + " to one of the target types " +
speed, string.Join(", ", targetTypes));
weights, }
metadata
); expr = exprSpec.Finalize();
}
if (expr.Finalize() == null)
private static AspectMetadata ParseAspect(this JsonElement e, string filepath, Context context) {
{ throw new NullReferenceException("The finalized form of expression `" + exprSpec + "` is null");
var expr = GetTopLevelExpression(e, context); }
var name = e.Get("name");
var targetTypes = new List<Type>(); if (expr.Types.Count() > 1)
foreach (var t in expr.Types) {
{ throw new ArgumentException("The aspect " + name + " is ambigous, it matches multiple types: " +
var a = Var.Fresh(t); string.Join(", ", expr.Types));
var b = Var.Fresh(new Curry(a, t)); }
if (t.Unify(new Curry(Typs.Tags, a)) != null && if (filepath != null && !(name + ".json").ToLower().Equals(filepath.ToLower()))
t.Unify(new Curry(Typs.Tags, new Curry(a, b))) == null {
) // Second should not match throw new ArgumentException($"Filename does not match the defined name: " +
{ $"filename is {filepath}, declared name is {name}");
// The target type is 'Tags -> a', where a is NOT a curry }
targetTypes.Add(t);
} var keys = (IEnumerable<string>) expr.PossibleTags()?.Keys ?? new List<string>();
} foreach (var key in keys)
{
if (targetTypes.Count == 0) if (!key.Trim().Equals(key))
{ {
throw new ArgumentException("The top level expression has types:\n" + Console.WriteLine($"Warning: a key can be trimmed: '{key}'");
string.Join("\n ", expr.Types) + }
"\nwhich can not be specialized into a form suiting `tags -> a`\n" + expr); }
}
return new AspectMetadata(
var exprSpec = expr.Specialize(targetTypes); expr,
if (expr == null) name,
{ e.Get("description"),
throw new Exception("Could not specialize the expression " + expr + " to one of the target types " + e.TryGet("author"),
string.Join(", ", targetTypes)); e.TryGet("unit"),
} filepath ?? "unknown"
);
expr = exprSpec.Finalize(); }
}
if (expr.Finalize() == null)
{
throw new NullReferenceException("The finalized form of expression `" + exprSpec + "` is null");
}
var name = e.Get("name");
if (expr.Types.Count() > 1)
{
throw new ArgumentException("The aspect " + name + " is ambigous, it matches multiple types: " +
string.Join(", ", expr.Types));
}
if (filepath != null && !(name + ".json").ToLower().Equals(filepath.ToLower()))
{
throw new ArgumentException($"Filename does not match the defined name: " +
$"filename is {filepath}, declared name is {name}");
}
return new AspectMetadata(
expr,
name,
e.Get("description"),
e.TryGet("author"),
e.TryGet("unit"),
filepath ?? "unknown"
);
}
private static string Get(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
throw new ArgumentException($"The obligated property {key} is missing");
}
private static string TryGet(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
return null;
}
}
} }

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.IO.jsonParser
{
public static partial class JsonParser
{
private static IExpression ParseProfileProperty(JsonElement e, Context c, string property)
{
try
{
var prop = e.GetProperty(property);
return ParseExpression(prop, c)
.Specialize(new Curry(Typs.Tags, new Var("a")))
.Optimize();
}
catch (Exception exc)
{
throw new Exception("While parsing the property " + property, exc);
}
}
private static Dictionary<string, IExpression> ParseParameters(this JsonElement e)
{
var ps = new Dictionary<string, IExpression>();
foreach (var obj in e.EnumerateObject())
{
var nm = obj.Name.TrimStart('#');
switch (obj.Value.ValueKind)
{
case JsonValueKind.String:
var v = obj.Value.ToString();
if (v.Equals("yes") || v.Equals("no"))
{
ps[nm] = new Constant(Typs.Bool, v);
}
else
{
ps[nm] = new Constant(v);
}
break;
case JsonValueKind.Number:
ps[nm] = new Constant(obj.Value.GetDouble());
break;
case JsonValueKind.True:
ps[nm] = new Constant(Typs.Bool, "yes");
break;
case JsonValueKind.False:
ps[nm] = new Constant(Typs.Bool, "yes");
break;
case JsonValueKind.Array:
var list = obj.Value.EnumerateArray().Select(e => e.ToString()).ToList();
ps[nm] = new Constant(new ListType(Typs.String),list);
break;
default:
throw new ArgumentException(
"Parameters are not allowed to be complex expressions, they should be simple values. " +
"Simplify the value for parameter " + obj.Name);
}
}
return ps;
}
private static string Get(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
throw new ArgumentException($"The obligated property {key} is missing");
}
private static string TryGet(this JsonElement json, string key)
{
if (json.TryGetProperty(key, out var p))
{
return p.GetString();
}
return null;
}
}
}

View file

@ -0,0 +1,3 @@
function const(a, b)
return a
end

View file

@ -0,0 +1,9 @@
function containedIn(list, a)
for _, value in ipairs(list) do
if (value == a) then
return true
end
end
return false;
end

View file

@ -1,4 +1,8 @@
function double_compare(a, b) function double_compare(a, b)
if (b == nil) then
return false
end
if (type(a) ~= "number") then if (type(a) ~= "number") then
a = parse(a) a = parse(a)
end end
@ -6,5 +10,9 @@ function double_compare(a, b)
if(type(b) ~= "number") then if(type(b) ~= "number") then
b = parse(b) b = parse(b)
end end
return math.abs(a - b) > 0.001 if (a == b) then
return true
end
return math.abs(a - b) < 0.0001
end end

View file

@ -1,3 +1,10 @@
function member_of() function member_of(calledIn, parameters, tags, result)
??? local k = "_relation:" .. calledIn
-- This tag is conventiently setup by all the preprocessors, which take the parameters into account
local doesMatch = tags[k]
if (doesMatch == "yes") then
result.attributes_to_keep[k] = "yes"
return true
end
return false
end end

View file

@ -1,5 +1,4 @@
function must_match(tags, result, needed_keys, table) function must_match(tags, result, needed_keys, table)
local result_list = {}
for _, key in ipairs(needed_keys) do for _, key in ipairs(needed_keys) do
local v = tags[key] local v = tags[key]
if (v == nil) then if (v == nil) then
@ -9,17 +8,30 @@ function must_match(tags, result, needed_keys, table)
local mapping = table[key] local mapping = table[key]
if (type(mapping) == "table") then if (type(mapping) == "table") then
local resultValue = mapping[v] local resultValue = mapping[v]
if (v == nil or v == false) then if (resultValue == nil or
resultValue == false or
resultValue == "no" or
resultValue == "false") then
return false return false
end end
if (v == "no" or v == "false") then elseif (type(mapping) == "string") then
local bool = mapping
if (bool == "yes" or bool == "1") then
return true
elseif (bool == "no" or bool == "0") then
return false return false
end end
error("MustMatch got a string value it can't handle: " .. bool)
result.attributes_to_keep[key] = v
else else
error("The mapping is not a table. This is not supported") error("The mapping is not a table. This is not supported. We got " .. mapping)
end end
end end
-- Now that we know for sure that every key matches, we add them all
for _, key in ipairs(needed_keys) do
local v = tags[key]
result.attributes_to_keep[key] = v
end
return true; return true;
end end

View file

@ -1,4 +1,8 @@
function notEq(a, b) function notEq(a, b)
if (b == nil) then
b = "yes"
end
if (a ~= b) then if (a ~= b) then
return "yes" return "yes"
else else

View file

@ -0,0 +1,20 @@
function string.start(strt, s)
return string.sub(s, 1, string.len(strt)) == strt
end
-- every key starting with "_relation:<name>:XXX" is rewritten to "_relation:XXX"
function remove_relation_prefix(tags, name)
local new_tags = {}
for k, v in pairs(tags) do
local prefix = "_relation:" .. name;
if (string.start(prefix, k)) then
local new_key = "_relation:" .. string.sub(k, string.len(prefix))
new_tags[new_key] = v
else
new_tags[k] = v
end
end
return new_tags
end

View file

@ -3,36 +3,57 @@ failed_profile_tests = false
expected should be a table containing 'access', 'speed' and 'weight' expected should be a table containing 'access', 'speed' and 'weight'
]] ]]
function unit_test_profile(profile_function, profile_name, index, expected, tags) function unit_test_profile(profile_function, profile_name, index, expected, tags)
result = {attributes_to_keep = {}} local result = { attributes_to_keep = {} }
local profile_failed = false
profile_function(tags, result) profile_function(tags, result)
if (result.access ~= expected.access) then local accessCorrect = (result.access == 0 and expected.access == "no") or result.access == 1
if (not accessCorrect) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".access: expected " .. expected.access .. " but got " .. result.access) print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".access: expected " .. expected.access .. " but got " .. result.access)
profile_failed = true
failed_profile_tests = true failed_profile_tests = true
end end
if (result.access == 0) then if (expected.access == "no") then
-- we cannot access this road, the other results are irrelevant -- we cannot access this road, the other results are irrelevant
if (profile_failed) then
print("The used tags for test " .. tostring(index) .. " are:")
debug_table(tags)
end
return return
end end
if (double_compare(result.speed, expected.speed)) then if (not double_compare(result.speed, expected.speed)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".speed: expected " .. expected.speed .. " but got " .. result.speed) print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".speed: expected " .. expected.speed .. " but got " .. result.speed)
failed_profile_tests = true failed_profile_tests = true
profile_failed = true
end end
if (double_compare(result.oneway, expected.oneway)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but got " .. result.oneway) local actualOneway = result.oneway;
failed_profile_tests = true if (result.oneway == 0) then
actualOneway = "both"
elseif (result.oneway == 1) then
actualOneway = "with"
elseif (result.oneway == 2) then
actualOneway = "against"
end end
if (double_compare(result.oneway, expected.oneway)) then if (expected.oneway ~= actualOneway) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but got " .. result.oneway) print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but got " .. actualOneway)
failed_profile_tests = true failed_profile_tests = true
profile_failed = true
end end
if (double_compare(inv(result.factor), 0.033333)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".factor: expected " .. expected.weight .. " but got " .. inv(result.factor)) if (not double_compare(result.factor, expected.weight)) then
print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".factor: expected " .. expected.weight .. " but got " .. result.factor)
failed_profile_tests = true failed_profile_tests = true
profile_failed = true
end
if (profile_failed == true) then
print("The used tags for test " .. tostring(index) .. " are:")
debug_table(tags)
end end
end end

View file

@ -17,6 +17,7 @@
- parse - parse
- to_string - to_string
- concat - concat
- containedIn
- min - min
- max - max
- sum - sum
@ -40,7 +41,7 @@
#### eq #### eq
Argument name | | | Argument name | | |
--------------| - | - | - -------------- | - | - | -
**a** | $a | $a | **a** | $a | $a |
**b** | $a | $a | **b** | $a | $a |
_return type_ | bool | string | _return type_ | bool | string |
@ -66,14 +67,14 @@ end
#### notEq #### notEq
Argument name | | | Argument name | | | |
--------------| - | - | - -------------- | - | - | - | -
**a** | $a | $a | **a** | $a | $a | bool |
**b** | $a | $a | **b** | $a | $a | _none_ |
_return type_ | bool | string | _return type_ | bool | string | bool |
Returns 'yes' if the two passed in values are _not_ the same OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;
@ -81,6 +82,10 @@ Lua implementation:
````lua ````lua
function notEq(a, b) function notEq(a, b)
if (b == nil) then
b = "yes"
end
if (a ~= b) then if (a ~= b) then
return "yes" return "yes"
else else
@ -92,14 +97,14 @@ end
#### not #### not
Argument name | | | Argument name | | | |
--------------| - | - | - -------------- | - | - | - | -
**a** | $a | $a | **a** | $a | $a | bool |
**b** | $a | $a | **b** | $a | $a | _none_ |
_return type_ | bool | string | _return type_ | bool | string | bool |
Returns 'yes' if the two passed in values are _not_ the same OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;
@ -107,6 +112,10 @@ Lua implementation:
````lua ````lua
function notEq(a, b) function notEq(a, b)
if (b == nil) then
b = "yes"
end
if (a ~= b) then if (a ~= b) then
return "yes" return "yes"
else else
@ -119,7 +128,7 @@ end
#### inv #### inv
Argument name | | | Argument name | | |
--------------| - | - | - -------------- | - | - | -
**d** | pdouble | double | **d** | pdouble | double |
_return type_ | pdouble | double | _return type_ | pdouble | double |
@ -140,7 +149,7 @@ end
#### default #### default
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**defaultValue** | $a | **defaultValue** | $a |
**f** | $b -> $a | **f** | $b -> $a |
_return type_ | $b -> $a | _return type_ | $b -> $a |
@ -164,10 +173,10 @@ end
#### parse #### parse
Argument name | | Argument name | | |
--------------| - | - -------------- | - | - | -
**s** | string | **s** | string | string |
_return type_ | double | _return type_ | double | pdouble |
Parses a string into a numerical value Parses a string into a numerical value
@ -210,7 +219,7 @@ end
#### to_string #### to_string
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**obj** | $a | **obj** | $a |
_return type_ | string | _return type_ | string |
@ -231,7 +240,7 @@ end
#### concat #### concat
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**a** | string | **a** | string |
**b** | string | **b** | string |
_return type_ | string | _return type_ | string |
@ -250,10 +259,38 @@ end
```` ````
#### containedIn
Argument name | |
-------------- | - | -
**list** | list ($a) |
**a** | $a |
_return type_ | bool |
Given a list of values, checks if the argument is contained in the list.
Lua implementation:
````lua
function containedIn(list, a)
for _, value in ipairs(list) do
if (value == a) then
return true
end
end
return false;
end
````
#### min #### min
Argument name | | | | | | Argument name | | | | | |
--------------| - | - | - | - | - | - -------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) | **list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | bool | _return type_ | nat | int | pdouble | double | bool |
@ -283,7 +320,7 @@ end
#### max #### max
Argument name | | | | | | Argument name | | | | | |
--------------| - | - | - | - | - | - -------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) | **list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | bool | _return type_ | nat | int | pdouble | double | bool |
@ -313,7 +350,7 @@ end
#### sum #### sum
Argument name | | | | | | Argument name | | | | | |
--------------| - | - | - | - | - | - -------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) | **list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | int | _return type_ | nat | int | pdouble | double | int |
@ -341,7 +378,7 @@ end
#### multiply #### multiply
Argument name | | | | | | Argument name | | | | | |
--------------| - | - | - | - | - | - -------------- | - | - | - | - | - | -
**list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) | **list** | list (nat) | list (int) | list (pdouble) | list (double) | list (bool) |
_return type_ | nat | int | pdouble | double | bool | _return type_ | nat | int | pdouble | double | bool |
@ -366,7 +403,7 @@ end
#### firstMatchOf #### firstMatchOf
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**s** | list (string) | **s** | list (string) |
_return type_ | (tags -> list ($a)) -> tags -> $a | _return type_ | (tags -> list ($a)) -> tags -> $a |
@ -404,7 +441,7 @@ end
#### mustMatch #### mustMatch
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**neededKeys (filled in by parser)** | list (string) | **neededKeys (filled in by parser)** | list (string) |
**f** | tags -> list (bool) | **f** | tags -> list (bool) |
_return type_ | tags -> bool | _return type_ | tags -> bool |
@ -420,7 +457,6 @@ Lua implementation:
````lua ````lua
function must_match(tags, result, needed_keys, table) function must_match(tags, result, needed_keys, table)
local result_list = {}
for _, key in ipairs(needed_keys) do for _, key in ipairs(needed_keys) do
local v = tags[key] local v = tags[key]
if (v == nil) then if (v == nil) then
@ -430,18 +466,31 @@ function must_match(tags, result, needed_keys, table)
local mapping = table[key] local mapping = table[key]
if (type(mapping) == "table") then if (type(mapping) == "table") then
local resultValue = mapping[v] local resultValue = mapping[v]
if (v == nil or v == false) then if (resultValue == nil or
resultValue == false or
resultValue == "no" or
resultValue == "false") then
return false return false
end end
if (v == "no" or v == "false") then elseif (type(mapping) == "string") then
local bool = mapping
if (bool == "yes" or bool == "1") then
return true
elseif (bool == "no" or bool == "0") then
return false return false
end end
error("MustMatch got a string value it can't handle: " .. bool)
result.attributes_to_keep[key] = v
else else
error("The mapping is not a table. This is not supported") error("The mapping is not a table. This is not supported. We got " .. mapping)
end end
end end
-- Now that we know for sure that every key matches, we add them all
for _, key in ipairs(needed_keys) do
local v = tags[key]
result.attributes_to_keep[key] = v
end
return true; return true;
end end
```` ````
@ -449,22 +498,41 @@ end
#### memberOf #### memberOf
- (tags -> $a) -> tags -> list ($a) Argument name | |
-------------- | - | -
**f** | tags -> bool |
**tags** | tags |
_return type_ | bool |
This function uses memberships of relations to calculate values.
Consider all the relations the scrutinized way is part of.The enclosed function is executed for every single relation which is part of it, generating a list of results.This list of results is in turn returned by 'memberOf' This function returns true, if the way is member of a relation matching the specified function.
In itinero 1/lua, this is implemented by converting the matching relations and by adding the tags of the relations to the dictionary (or table) with the highway tags.The prefix is '_relation:n:key=value', where 'n' is a value between 0 and the number of matching relations (implying that all of these numbers are scanned).The matching relations can be extracted by the compiler for the preprocessing.
For testing, the relation can be emulated by using e.g. '_relation:0:key=value' In order to use this for itinero 1.0, the membership _must_ be the top level expression.
Conceptually, when the aspect is executed for a way, every relation will be used as argument in the subfunction `f`
If this subfunction returns 'true', the entire aspect will return true.
In the lua implementation for itinero 1.0, this is implemented slightly different: a flag `_relation:<aspect_name>="yes"` will be set if the aspect matches on every way for where this aspect matches.
However, this plays poorly with parameters (e.g.: what if we want to cycle over a highway which is part of a certain cycling network with a certain `#network_name`?) Luckily, parameters can only be simple values. To work around this problem, an extra tag is introduced for _every single profile_:`_relation:<profile_name>:<aspect_name>=yes'. The subfunction is thus executed `countOr(relations) * countOf(profiles)` time, yielding `countOf(profiles)` tags. The profile function then picks the tags for himself and strips the `<profile_name>:` away from the key.
In the test.csv, one can simply use `_relation:<aspect_name>=yes` to mimic relations in your tests
Lua implementation: Lua implementation:
````lua ````lua
function member_of() function member_of(calledIn, parameters, tags, result)
??? local k = "_relation:" .. calledIn
-- This tag is conventiently setup by all the preprocessors, which take the parameters into account
local doesMatch = tags[k]
if (doesMatch == "yes") then
result.attributes_to_keep[k] = "yes"
return true
end
return false
end end
```` ````
@ -472,7 +540,7 @@ end
#### if_then_else #### if_then_else
Argument name | | | Argument name | | |
--------------| - | - | - -------------- | - | - | -
**condition** | bool | bool | **condition** | bool | bool |
**then** | $a | $a | **then** | $a | $a |
**else** | $a | _none_ | **else** | $a | _none_ |
@ -499,7 +567,7 @@ end
#### if #### if
Argument name | | | Argument name | | |
--------------| - | - | - -------------- | - | - | -
**condition** | bool | bool | **condition** | bool | bool |
**then** | $a | $a | **then** | $a | $a |
**else** | $a | _none_ | **else** | $a | _none_ |
@ -526,7 +594,7 @@ end
#### id #### id
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**a** | $a | **a** | $a |
_return type_ | $a | _return type_ | $a |
@ -547,7 +615,7 @@ end
#### const #### const
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**a** | $a | **a** | $a |
**b** | $b | **b** | $b |
_return type_ | $a | _return type_ | $a |
@ -560,14 +628,16 @@ Small utility function, which takes two arguments `a` and `b` and returns `a`. U
Lua implementation: Lua implementation:
````lua ````lua
function const(a, b)
return a
end
```` ````
#### constRight #### constRight
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**a** | $a | **a** | $a |
**b** | $b | **b** | $b |
_return type_ | $b | _return type_ | $b |
@ -587,11 +657,11 @@ Lua implementation:
#### dot #### dot
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**f** | $gType -> $arg | **f** | $b -> $c |
**g** | $fType -> $gType | **g** | $a -> $b |
**a** | $fType | **a** | $a |
_return type_ | $arg | _return type_ | $c |
Higher order function: converts `f (g a)` into `(dot f g) a`. In other words, this fuses `f` and `g` in a new function, which allows the argument to be lifted out of the expression Higher order function: converts `f (g a)` into `(dot f g) a`. In other words, this fuses `f` and `g` in a new function, which allows the argument to be lifted out of the expression
@ -608,7 +678,7 @@ Lua implementation:
#### listDot #### listDot
Argument name | | Argument name | |
--------------| - | - -------------- | - | -
**list** | list ($a -> $b) | **list** | list ($a -> $b) |
**a** | $a | **a** | $a |
_return type_ | list ($b) | _return type_ | list ($b) |
@ -628,10 +698,19 @@ Lua implementation:
#### eitherFunc #### eitherFunc
- ($a -> $b) -> ($c -> $d) -> $a -> $b Argument name | | |
- ($a -> $b) -> ($c -> $d) -> $c -> $d -------------- | - | - | -
**f** | $a -> $b | $a -> $b |
**g** | $c -> $d | $c -> $d |
**a** | $a | $c |
_return type_ | $b | $d |
EitherFunc is a small utility function, mostly used in the parser. It allows the compiler to choose a function, based on the types.
Consider the mapping `{'someKey':'someValue'}`. Under normal circumstances, this acts as a pointwise-function, converting the string `someKey` into `someValue`, just like an ordinary dictionary would do. However, in the context of `mustMatch`, we would prefer this to act as a _check_, that the highway _has_ a key `someKey` which is `someValue`, thus acting as `{'someKey': {'$eq':'someValue'}}. Both behaviours are automatically supported in parsing, by parsing the string as `(eitherFunc id eq) 'someValue'`. The type system is then able to figure out which implementation is needed.
Disclaimer: _you should never ever need this in your profiles_
@ -644,9 +723,14 @@ Lua implementation:
#### stringToTags #### stringToTags
- (string -> string -> $a) -> tags -> list ($a) Argument name | |
-------------- | - | -
**f** | string -> string -> $a |
**tags** | tags |
_return type_ | list ($a) |
stringToTags converts a function `string -> string -> a` into a function `tags -> [a]`

View file

@ -1,11 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Functions; using AspectedRouting.Language.Expression;
using AspectedRouting.Typ; using AspectedRouting.Language.Functions;
using static AspectedRouting.Deconstruct; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting namespace AspectedRouting.Language
{ {
public static class Analysis public static class Analysis
{ {
@ -26,6 +27,7 @@ namespace AspectedRouting
Console.WriteLine(e); Console.WriteLine(e);
var keys = e.PossibleTags().Keys.ToList(); var keys = e.PossibleTags().Keys.ToList();
var results = possibleTags.OnAllCombinations( var results = possibleTags.OnAllCombinations(
tags => tags =>
{ {
@ -124,10 +126,10 @@ namespace AspectedRouting
} while (SelectNext()); } while (SelectNext());
} }
public static Dictionary<string, (IEnumerable<Typ.Type> Types, string inFunction)> UsedParameters( public static Dictionary<string, (List<Type> Types, string inFunction)> UsedParameters(
this ProfileMetaData profile, Context context) this ProfileMetaData profile, Context context)
{ {
var parameters = new Dictionary<string, (IEnumerable<Typ.Type> Types, string inFunction)>(); var parameters = new Dictionary<string, (List<Type> Types, string inFunction)>();
void AddParams(IExpression e, string inFunction) void AddParams(IExpression e, string inFunction)
{ {
@ -149,7 +151,7 @@ namespace AspectedRouting
} }
else else
{ {
parameters[param.ParamName] = (param.Types, inFunction); parameters[param.ParamName] = (param.Types.ToList(), inFunction);
} }
} }
} }
@ -159,6 +161,12 @@ namespace AspectedRouting
AddParams(profile.Oneway, profile.Name + ".oneway"); AddParams(profile.Oneway, profile.Name + ".oneway");
AddParams(profile.Speed, profile.Name + ".speed"); AddParams(profile.Speed, profile.Name + ".speed");
foreach (var (key, expr) in profile.Priority)
{
AddParams(new Parameter(key), profile.Name + ".priority.lefthand");
AddParams(expr, profile.Name + ".priority");
}
foreach (var (name, expr) in context.DefinedFunctions) foreach (var (name, expr) in context.DefinedFunctions)
{ {
AddParams(expr, name); AddParams(expr, name);
@ -194,11 +202,11 @@ namespace AspectedRouting
return text; return text;
} }
public static void SanityCheckProfile(this ProfileMetaData pmd) public static void SanityCheckProfile(this ProfileMetaData pmd, Context context)
{ {
var defaultParameters = pmd.DefaultParameters.Keys; var defaultParameters = pmd.DefaultParameters.Keys;
var usedParameters = pmd.UsedParameters(new Context()).Keys.Select(key => key.TrimStart('#')); var usedParameters = pmd.UsedParameters(context).Keys.Select(key => key.TrimStart('#'));
var diff = usedParameters.ToHashSet().Except(defaultParameters).ToList(); var diff = usedParameters.ToHashSet().Except(defaultParameters).ToList();
if (diff.Any()) if (diff.Any())
@ -206,28 +214,48 @@ namespace AspectedRouting
throw new ArgumentException("No default value set for parameter " + string.Join(", ", diff)); throw new ArgumentException("No default value set for parameter " + string.Join(", ", diff));
} }
foreach (var (profileName, profileParams) in pmd.Profiles) var unused = defaultParameters.Except(usedParameters);
if (unused.Any())
{
throw new ArgumentException("A default value is set for parameter, but it is unused: " +
string.Join(", ", unused));
}
foreach (var (behaviourName, behaviourParams) in pmd.Behaviours)
{ {
var sum = 0.0; var sum = 0.0;
foreach (var (paramName, _) in pmd.Weights) var explanation = "";
foreach (var (paramName, _) in pmd.Priority)
{ {
if (!profileParams.TryGetValue(paramName, out var weight))
if (!pmd.DefaultParameters.ContainsKey(paramName))
{ {
throw new ArgumentException(
$"The behaviour {behaviourName} uses a parameter for which no default is set: {paramName}");
}
if (!behaviourParams.TryGetValue(paramName, out var weight))
{
explanation += $"\n - {paramName} = default (not set)";
continue; continue;
} }
if (!(weight is double d)) var weightObj = weight.Evaluate(context);
if (!(weightObj is double d))
{ {
continue; throw new ArgumentException($"The parameter {paramName} is not a numeric value");
} }
sum += Math.Abs(d); sum += Math.Abs(d);
explanation += $"\n - {paramName} = {d}";
} }
if (Math.Abs(sum) < 0.0001) if (Math.Abs(sum) < 0.0001)
{ {
throw new ArgumentException("Profile " + profileName + throw new ArgumentException("Profile " + behaviourName +
": the summed parameters to calculate the weight are zero or very low"); ": the summed parameters to calculate the weight are zero or very low:" +
explanation);
} }
} }
} }
@ -238,9 +266,9 @@ namespace AspectedRouting
{ {
var order = new List<IExpression>(); var order = new List<IExpression>();
var mapping = new List<IExpression>(); var mapping = new List<IExpression>();
if (UnApply( if (Deconstruct.UnApply(
UnApply(IsFunc(Funcs.FirstOf), Assign(order)), Deconstruct.UnApply(Deconstruct.IsFunc(Funcs.FirstOf), Deconstruct.Assign(order)),
Assign(mapping) Deconstruct.Assign(mapping)
).Invoke(expr)) ).Invoke(expr))
{ {
var expectedKeys = ((IEnumerable<object>) order.First().Evaluate(null)).Select(o => var expectedKeys = ((IEnumerable<object>) order.First().Evaluate(null)).Select(o =>
@ -339,5 +367,61 @@ namespace AspectedRouting
return result; return result;
} }
public static Dictionary<string, IExpression> MembershipMappingsFor(ProfileMetaData profile, Context context)
{
var calledFunctions = profile.Priority.Values.ToHashSet();
calledFunctions.Add(profile.Speed);
calledFunctions.Add(profile.Access);
calledFunctions.Add(profile.Oneway);
var calledFunctionQueue = new Queue<string>();
var alreadyAnalysedFunctions = new HashSet<string>();
var memberships = new Dictionary<string, IExpression>();
void HandleExpression(IExpression e, string calledIn)
{
e.Visit(f =>
{
var mapping = new List<IExpression>();
if (Deconstruct.UnApply(Deconstruct.IsFunc(Funcs.MemberOf),
Deconstruct.Assign(mapping)
).Invoke(f))
{
memberships.Add(calledIn, mapping.First());
return false;
}
if (f is FunctionCall fc)
{
calledFunctionQueue.Enqueue(fc.CalledFunctionName);
}
return true;
});
}
foreach (var e in calledFunctions)
{
HandleExpression(e, "profile_root");
}
while (calledFunctionQueue.TryDequeue(out var functionName))
{
if (alreadyAnalysedFunctions.Contains(functionName))
{
continue;
}
alreadyAnalysedFunctions.Add(functionName);
var functionImplementation = context.GetFunction(functionName);
HandleExpression(functionImplementation, functionName);
}
return memberships;
}
} }
} }

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
namespace AspectedRouting.Language
{
public class Context
{
public readonly Dictionary<string, IExpression> Parameters = new Dictionary<string, IExpression>();
public readonly Dictionary<string, AspectMetadata> DefinedFunctions = new Dictionary<string, AspectMetadata>();
public readonly string AspectName;
public Context()
{
}
protected Context(string aspectName, Dictionary<string, IExpression> parameters,
Dictionary<string, AspectMetadata> definedFunctions)
{
AspectName = aspectName;
Parameters = parameters;
DefinedFunctions = definedFunctions;
}
public Context(Context c) : this(c.AspectName, c.Parameters, c.DefinedFunctions)
{
}
public void AddParameter(string name, string value)
{
Parameters.Add(name, new Constant(value));
}
public void AddFunction(string name, AspectMetadata function)
{
if (Funcs.Builtins.ContainsKey(name))
{
throw new ArgumentException("Function " + name + " already exists, it is a builtin function");
}
if (DefinedFunctions.ContainsKey(name) && !function.ProfileInternal)
{
throw new ArgumentException("Function " + name + " already exists");
}
DefinedFunctions[name] = function;
}
public IExpression GetFunction(string name)
{
if (name.StartsWith("$"))
{
name = name.Substring(1);
}
if (Funcs.Builtins.ContainsKey(name))
{
return Funcs.Builtins[name];
}
if (DefinedFunctions.ContainsKey(name))
{
return DefinedFunctions[name];
}
throw new ArgumentException(
$"The function {name} is not a defined nor builtin function. Known functions are " +
string.Join(", ", DefinedFunctions.Keys));
}
public Context WithParameters(Dictionary<string, IExpression> parameters)
{
return new Context(AspectName, parameters, DefinedFunctions);
}
public Context WithAspectName(string name)
{
return new Context(name, Parameters, DefinedFunctions);
}
}
}

View file

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Functions; using AspectedRouting.Language.Expression;
namespace AspectedRouting namespace AspectedRouting.Language
{ {
public static class Deconstruct public static class Deconstruct
{ {
@ -99,9 +99,7 @@ namespace AspectedRouting
}; };
} }
public static Func<IExpression, bool> Any() public static readonly Func<IExpression, bool> Any = e => true;
{
return e => true;
}
} }
} }

View file

@ -1,11 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Functions;
using static AspectedRouting.Deconstruct; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Expression
{ {
public class Apply : IExpression public class Apply : IExpression
{ {
@ -88,7 +88,7 @@ namespace AspectedRouting.Functions
$"is applied on an argument with types:" + $"is applied on an argument with types:" +
$"{string.Join(", ", argument.Optimize().Types)}"; $"{string.Join(", ", argument.Optimize().Types)}";
} }
catch (Exception e) catch (Exception)
{ {
_debugInfo = $"\n{f.TypeBreakdown()}\n" + _debugInfo = $"\n{f.TypeBreakdown()}\n" +
$"{argument.TypeBreakdown()}"; $"{argument.TypeBreakdown()}";
@ -99,9 +99,9 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] arguments) public object Evaluate(Context c, params IExpression[] arguments)
{ {
if (Types.Count() > 1) if (!Types.Any())
{ {
// We try to select the smallest type throw new ArgumentException("Trying to invoke an invalid expression: " + this);
} }
var type = Types.First(); var type = Types.First();
@ -166,20 +166,20 @@ namespace AspectedRouting.Functions
// => (const dot _) id => dot id => id // => (const dot _) id => dot id => id
// or => (constRight _ id) id => id id => id // or => (constRight _ id) id => id id => id
if ( if (
UnApplyAny( Deconstruct.UnApplyAny(
UnApplyAny( Deconstruct.UnApplyAny(
UnApplyAny( Deconstruct.UnApplyAny(
IsFunc(Funcs.Const), Deconstruct.IsFunc(Funcs.Const),
IsFunc(Funcs.Dot)), Deconstruct.IsFunc(Funcs.Dot)),
Any()), Deconstruct.Any),
IsFunc(Funcs.Id) Deconstruct.IsFunc(Funcs.Id)
).Invoke(this) ).Invoke(this)
&& UnApplyAny(UnApplyAny( && Deconstruct.UnApplyAny(Deconstruct.UnApplyAny(
UnApplyAny( Deconstruct.UnApplyAny(
IsFunc(Funcs.ConstRight), Deconstruct.IsFunc(Funcs.ConstRight),
Any()), Deconstruct.Any),
IsFunc(Funcs.Id)), Deconstruct.IsFunc(Funcs.Id)),
IsFunc(Funcs.Id) Deconstruct.IsFunc(Funcs.Id)
).Invoke(this)) ).Invoke(this))
{ {
return Funcs.Id; return Funcs.Id;
@ -247,12 +247,12 @@ namespace AspectedRouting.Functions
// ((dot f0) f1) // ((dot f0) f1)
// ((dot f0) f1) arg is the actual expression, but arg is already split of // ((dot f0) f1) arg is the actual expression, but arg is already split of
if (UnApply( if (Deconstruct.UnApply(
UnApply( Deconstruct.UnApply(
IsFunc(Funcs.Dot), Deconstruct.IsFunc(Funcs.Dot),
Assign(f0) Deconstruct.Assign(f0)
), ),
Assign(f1)).Invoke(f) Deconstruct.Assign(f1)).Invoke(f)
) )
{ {
// f0 (f1 arg) // f0 (f1 arg)

View file

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Expression
{ {
public class AspectMetadata : IExpression public class AspectMetadata : IExpression
{ {
@ -32,6 +32,12 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] arguments) public object Evaluate(Context c, params IExpression[] arguments)
{ {
if (ProfileInternal && arguments.Length > 0)
{
var tags = (Dictionary<string, string>) arguments[0].Evaluate(c);
}
return ExpressionImplementation.Evaluate(c, arguments); return ExpressionImplementation.Evaluate(c, arguments);
} }

View file

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Expression
{ {
public abstract class Function : IExpression public abstract class Function : IExpression
{ {

View file

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Expression
{ {
public class FunctionCall : IExpression public class FunctionCall : IExpression
{ {
@ -34,7 +34,10 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] arguments) public object Evaluate(Context c, params IExpression[] arguments)
{ {
return c.GetFunction(_name).Evaluate(c, arguments);
var func = c.GetFunction(_name);
c = c.WithAspectName(_name);
return func.Evaluate(c, arguments);
} }
public IExpression Specialize(IEnumerable<Type> allowedTypes) public IExpression Specialize(IEnumerable<Type> allowedTypes)

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Expression
{ {
public class ProfileMetaData public class ProfileMetaData
{ {
@ -12,19 +12,19 @@ namespace AspectedRouting.Functions
public List<string> VehicleTyps { get; } public List<string> VehicleTyps { get; }
public List<string> Metadata { get; } public List<string> Metadata { get; }
public Dictionary<string, object> DefaultParameters { get; } public Dictionary<string, IExpression> DefaultParameters { get; }
public Dictionary<string, Dictionary<string, object>> Profiles { get; } public Dictionary<string, Dictionary<string, IExpression>> Behaviours { get; }
public IExpression Access { get; } public IExpression Access { get; }
public IExpression Oneway { get; } public IExpression Oneway { get; }
public IExpression Speed { get; } public IExpression Speed { get; }
public Dictionary<string, IExpression> Weights { get; } public Dictionary<string, IExpression> Priority { get; }
public ProfileMetaData(string name, string description, string author, string filename, public ProfileMetaData(string name, string description, string author, string filename,
List<string> vehicleTyps, Dictionary<string, object> defaultParameters, List<string> vehicleTyps, Dictionary<string, IExpression> defaultParameters,
Dictionary<string, Dictionary<string, object>> profiles, Dictionary<string, Dictionary<string, IExpression>> behaviours,
IExpression access, IExpression oneway, IExpression speed, IExpression access, IExpression oneway, IExpression speed,
Dictionary<string, IExpression> weights, List<string> metadata) Dictionary<string, IExpression> priority, List<string> metadata)
{ {
Name = name; Name = name;
Description = description; Description = description;
@ -34,16 +34,16 @@ namespace AspectedRouting.Functions
Access = access; Access = access;
Oneway = oneway; Oneway = oneway;
Speed = speed; Speed = speed;
Weights = weights; Priority = priority;
Metadata = metadata; Metadata = metadata;
DefaultParameters = defaultParameters; DefaultParameters = defaultParameters;
Profiles = profiles; Behaviours = behaviours;
} }
public override string ToString() public override string ToString()
{ {
return $"Profile: {Name} {Filename}\naccess={Access}\noneway={Oneway}\nspeed={Speed}\n" + return $"Profile: {Name} {Filename}\naccess={Access}\noneway={Oneway}\nspeed={Speed}\n" +
$"weights ={string.Join(" + ", Weights.Select(kv => "#" + kv.Key + " * " + kv.Value))} "; $"priorities = {string.Join(" + ", Priority.Select(kv => "#" + kv.Key + " * " + kv.Value))} ";
} }
} }
} }

View file

@ -1,11 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
namespace AspectedRouting.Functions namespace AspectedRouting.Language
{ {
public static class Funcs public static class Funcs
{ {
@ -23,12 +25,13 @@ namespace AspectedRouting.Functions
public static readonly Function ToStringFunc = new ToString(); public static readonly Function ToStringFunc = new ToString();
public static readonly Function Concat = new Concat(); public static readonly Function Concat = new Concat();
public static readonly Function ContainedIn = new ContainedIn();
public static readonly Function Min = new Min(); public static readonly Function Min = new Min();
public static readonly Function Max = new Max(); public static readonly Function Max = new Max();
public static readonly Function Sum = new Sum(); public static readonly Function Sum = new Sum();
public static readonly Function Multiply = new Multiply(); public static readonly Function Multiply = new Multiply();
public static readonly Function FirstOf = new FirstMatchOf(); public static readonly Function FirstOf = new FirstMatchOf();
public static readonly Function MustMatch = new MustMatch(); public static readonly Function MustMatch = new MustMatch();

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class All : Function public class All : Function
{ {

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Concat : Function public class Concat : Function
{ {

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Const : Function public class Const : Function
{ {

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class ConstRight : Function public class ConstRight : Function
{ public override string Description { get; } = { public override string Description { get; } =

View file

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Constant : IExpression public class Constant : IExpression
{ {
@ -40,7 +40,7 @@ namespace AspectedRouting.Functions
} }
catch (Exception e) catch (Exception e)
{ {
throw new Exception($"While creating a list with members {string.Join(", ", exprs)} {e.Message}", e); throw new Exception($"While creating a list with members {string.Join(", ", exprs.Select(e => e.Optimize()))} {e.Message}", e);
} }
} }

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Language.Functions
{
public class ContainedIn : Function
{
public override string Description { get; } =
"Given a list of values, checks if the argument is contained in the list.";
public override List<string> ArgNames { get; } = new List<string>{"list","a"};
public ContainedIn() : base("containedIn", true,
new[]
{
// [a] -> a -> bool
new Curry(
new ListType(new Var("a")),
new Curry(new Var("a"),
Typs.Bool))
}
)
{
}
private ContainedIn(IEnumerable<Type> types) : base("containedIn", types)
{
}
public override object Evaluate(Context c, params IExpression[] arguments)
{
var list = (IEnumerable<IExpression>) arguments[0].Evaluate(c);
var arg = arguments[1];
var result = new List<object>();
foreach (var f in list)
{
var o = f.Evaluate(c);
while (o is IExpression e)
{
o = e.Evaluate(c);
}
if (f.Equals(arg))
{
return true;
}
}
return result;
}
public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{
var unified = Types.SpecializeTo(allowedTypes);
if (unified == null)
{
return null;
}
return new ContainedIn(unified);
}
}
}

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Default : Function public class Default : Function
{ {

View file

@ -1,18 +1,19 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Dot : Function public class Dot : Function
{ {
public override string Description { get; } = "Higher order function: converts `f (g a)` into `(dot f g) a`. In other words, this fuses `f` and `g` in a new function, which allows the argument to be lifted out of the expression "; public override string Description { get; } = "Higher order function: converts `f (g a)` into `(dot f g) a`. In other words, this fuses `f` and `g` in a new function, which allows the argument to be lifted out of the expression ";
public override List<string> ArgNames { get; } = new List<string>{"f","g","a"}; public override List<string> ArgNames { get; } = new List<string>{"f","g","a"};
public static readonly Var A = new Var("fType"); public static readonly Var A = new Var("a");
public static readonly Var B = new Var("gType"); public static readonly Var B = new Var("b");
public static readonly Var C = new Var("arg"); public static readonly Var C = new Var("c");
public Dot() : base("dot", true, new[] public Dot() : base("dot", true, new[]
{ {
@ -34,7 +35,7 @@ namespace AspectedRouting.Functions
{ {
var f0 = arguments[0]; var f0 = arguments[0];
var f1 = arguments[1]; var f1 = arguments[1];
var resultType = (f1.Types.First() as Curry).ResultType; var resultType = ((Curry) f1.Types.First()).ResultType;
var a = arguments[2]; var a = arguments[2];
return f0.Evaluate(c, new Constant(resultType, f1.Evaluate(c, a))); return f0.Evaluate(c, new Constant(resultType, f1.Evaluate(c, a)));
} }

View file

@ -1,12 +1,24 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class EitherFunc : Function public class EitherFunc : Function
{ {
public override string Description { get; } =
"EitherFunc is a small utility function, mostly used in the parser. It allows the compiler to choose a function, based on the types.\n\n" +
"" +
"Consider the mapping `{'someKey':'someValue'}`. Under normal circumstances, this acts as a pointwise-function, converting the string `someKey` into `someValue`, just like an ordinary dictionary would do. " +
"However, in the context of `mustMatch`, we would prefer this to act as a _check_, that the highway _has_ a key `someKey` which is `someValue`, thus acting as " +
"`{'someKey': {'$eq':'someValue'}}. " +
"Both behaviours are automatically supported in parsing, by parsing the string as `(eitherFunc id eq) 'someValue'`. " +
"The type system is then able to figure out which implementation is needed.\n\n" +
"Disclaimer: _you should never ever need this in your profiles_";
public override List<string> ArgNames { get; } = new List<string>{"f","g","a"};
private static Var a = new Var("a"); private static Var a = new Var("a");
private static Var b = new Var("b"); private static Var b = new Var("b");
private static Var c = new Var("c"); private static Var c = new Var("c");

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Eq : Function public class Eq : Function
{ public override string Description { get; } = "Returns 'yes' if both values _are_ the same"; { public override string Description { get; } = "Returns 'yes' if both values _are_ the same";

View file

@ -1,9 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class FirstMatchOf : Function public class FirstMatchOf : Function
{ {

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Id : Function public class Id : Function
{ public override string Description { get; } = "Returns the argument unchanged - the identity function. Seems useless at first sight, but useful in parsing"; { public override string Description { get; } = "Returns the argument unchanged - the identity function. Seems useless at first sight, but useful in parsing";

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class If : Function public class If : Function
{ {

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Inv : Function public class Inv : Function
{ {

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class ListDot : Function public class ListDot : Function
{ {

View file

@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Mapping : Function public class Mapping : Function
{ {
@ -78,7 +79,6 @@ namespace AspectedRouting.Functions
var otherARgs = arguments.ToList().GetRange(1, arguments.Length - 1); var otherARgs = arguments.ToList().GetRange(1, arguments.Length - 1);
if (!StringToResultFunctions.TryGetValue(key, out var resultFunction)) if (!StringToResultFunctions.TryGetValue(key, out var resultFunction))
{ {
Console.WriteLine($"Warning: key {key} not found in mapping {this}");
return null; return null;
} }
@ -95,7 +95,7 @@ namespace AspectedRouting.Functions
var typeOptStr = string.Join(";", opt.Types); var typeOptStr = string.Join(";", opt.Types);
var typeEStr = string.Join("; ", e.Types); var typeEStr = string.Join("; ", e.Types);
if (opt == null || !opt.Types.Any()) if (!opt.Types.Any())
{ {
throw new NullReferenceException($"Optimized version is null, has different or empty types: " + throw new NullReferenceException($"Optimized version is null, has different or empty types: " +
$"\n{typeEStr}" + $"\n{typeEStr}" +

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Max : Function public class Max : Function
{ {

View file

@ -0,0 +1,72 @@
using System.Collections.Generic;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Language.Functions
{
public class MemberOf : Function
{
public override string Description { get; } =
"This function returns true, if the way is member of a relation matching the specified function.\n" +
"\n" +
"In order to use this for itinero 1.0, the membership _must_ be the top level expression.\n" +
"\n" +
"Conceptually, when the aspect is executed for a way, every relation will be used as argument in the subfunction `f`\n" +
"If this subfunction returns 'true', the entire aspect will return true.\n\n" +
"In the lua implementation for itinero 1.0, this is implemented slightly different:" +
" a flag `_relation:<aspect_name>=\"yes\"` will be set if the aspect matches on every way for where this aspect matches.\n" +
"However, this plays poorly with parameters (e.g.: what if we want to cycle over a highway which is part of a certain cycling network with a certain `#network_name`?) " +
"Luckily, parameters can only be simple values. To work around this problem, an extra tag is introduced for _every single profile_:" +
"`_relation:<profile_name>:<aspect_name>=yes'. The subfunction is thus executed `countOr(relations) * countOf(profiles)` time, yielding `countOf(profiles)` tags." +
" The profile function then picks the tags for himself and strips the `<profile_name>:` away from the key.\n\n" +
"\n\n" +
"In the test.csv, one can simply use `_relation:<aspect_name>=yes` to mimic relations in your tests";
public override List<string> ArgNames { get; } = new List<string>
{
"f","tags"
};
public MemberOf() : base(
"memberOf", true,
new[]
{
new Curry(
new Curry(Typs.Tags, Typs.Bool),
new Curry(Typs.Tags, Typs.Bool))
}
)
{
}
public MemberOf(IEnumerable<Type> types) : base("memberOf", types)
{
}
public override object Evaluate(Context c, params IExpression[] arguments)
{
// var subFunction = arguments[0];
var tags =(Dictionary<string, string>) arguments[1].Evaluate(c);
var name = c.AspectName.TrimStart('$');
if(tags.TryGetValue("_relation:"+name, out var v))
{
return v;
}
return "no";
}
public override IExpression Specialize(IEnumerable<Type> allowedTypes)
{
var unified = Types.SpecializeTo(allowedTypes);
if (unified == null)
{
return null;
}
return new MemberOf(unified);
}
}
}

View file

@ -1,13 +1,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Min : Function public class Min : Function
{ {
public override string Description { get; } = "Out of a list of values, gets the smallest value. IN case of a list of bools, this acts as `and`"; public override string Description { get; } =
public override List<string> ArgNames { get; } = new List<string>{"list"}; "Out of a list of values, gets the smallest value. IN case of a list of bools, this acts as `and`";
public override List<string> ArgNames { get; } = new List<string> {"list"};
public Min() : base("min", true, public Min() : base("min", true,
new[] new[]
@ -17,7 +20,6 @@ namespace AspectedRouting.Functions
new Curry(new ListType(Typs.PDouble), Typs.PDouble), new Curry(new ListType(Typs.PDouble), Typs.PDouble),
new Curry(new ListType(Typs.Double), Typs.Double), new Curry(new ListType(Typs.Double), Typs.Double),
new Curry(new ListType(Typs.Bool), Typs.Bool), new Curry(new ListType(Typs.Bool), Typs.Bool),
}) })
{ {
} }
@ -41,11 +43,12 @@ namespace AspectedRouting.Functions
public override object Evaluate(Context c, params IExpression[] arguments) public override object Evaluate(Context c, params IExpression[] arguments)
{ {
var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o!=null); var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o != null);
var expectedType = (Types.First() as Curry).ResultType; var expectedType = ((Curry) Types.First()).ResultType;
switch (expectedType) switch (expectedType)
{ case BoolType _: {
case BoolType _:
if (ls.Select(o => o.Equals("yes") || o.Equals("true")).All(b => b)) if (ls.Select(o => o.Equals("yes") || o.Equals("true")).All(b => b))
{ {
return "yes"; return "yes";
@ -54,9 +57,27 @@ namespace AspectedRouting.Functions
return "no"; return "no";
case DoubleType _: case DoubleType _:
case PDoubleType _: case PDoubleType _:
return ls.Select(o => (double) o).Min();
return ls.Select(o =>
{
while (o is IExpression e)
{
o = e.Evaluate(c);
}
return (double) o;
}).Min();
default: default:
return ls.Select(o => (int) o).Min(); return ls.Select(o =>
{
while (o is IExpression e)
{
o = e.Evaluate(c);
}
return (int) o;
}).Min();
} }
} }
} }

View file

@ -1,16 +1,18 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Multiply : Function public class Multiply : Function
{ {
public override string Description { get; } =
public override string Description { get; } = "Multiplies all the values in a given list. On a list of booleans, this acts as 'and' or 'all'"; "Multiplies all the values in a given list. On a list of booleans, this acts as 'and' or 'all'";
public override List<string> ArgNames { get; } = new List<string> {"list"}; public override List<string> ArgNames { get; } = new List<string> {"list"};
public Multiply() : base("multiply", true, public Multiply() : base("multiply", true,
new[] new[]
{ {
@ -42,19 +44,26 @@ namespace AspectedRouting.Functions
public override object Evaluate(Context c, params IExpression[] arguments) public override object Evaluate(Context c, params IExpression[] arguments)
{ {
var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o != null); var ls = ((IEnumerable<object>) arguments[0].Evaluate(c)).Where(o => o != null);
var expectedType = (Types.First() as Curry).ResultType; var expectedType = ((Curry) Types.First()).ResultType;
switch (expectedType) switch (expectedType)
{ {
case BoolType _: case BoolType _:
foreach (var o in ls) foreach (var oo in ls)
{ {
if(!(o is string s)) var o = oo;
while (o is IExpression e)
{
o = e.Evaluate(c);
}
if (!(o is string s))
{ {
return "no"; return "no";
} }
if(!(o.Equals("yes") || o.Equals("true")))
if (!(s.Equals("yes") || s.Equals("true")))
{ {
return "no"; return "no";
} }
@ -64,16 +73,28 @@ namespace AspectedRouting.Functions
case DoubleType _: case DoubleType _:
case PDoubleType _: case PDoubleType _:
var mult = 1.0; var mult = 1.0;
foreach (var o in ls) foreach (var oo in ls)
{ {
var o = oo;
while (o is IExpression e)
{
o = e.Evaluate(c);
}
mult *= (double) o; mult *= (double) o;
} }
return mult; return mult;
default: default:
var multI = 1; var multI = 1;
foreach (var o in ls) foreach (var oo in ls)
{ {
var o = oo;
while (o is IExpression e)
{
o = e.Evaluate(c);
}
multI *= (int) o; multI *= (int) o;
} }

View file

@ -1,9 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using Type = AspectedRouting.Typ.Type; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class MustMatch : Function public class MustMatch : Function
{ {

View file

@ -1,18 +1,20 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class NotEq : Function public class NotEq : Function
{ {
public override string Description { get; } = "Returns 'yes' if the two passed in values are _not_ the same"; public override string Description { get; } = "OVerloaded function, either boolean not or returns 'yes' if the two passed in values are _not_ the same;";
public override List<string> ArgNames { get; } = new List<string> {"a", "b"}; public override List<string> ArgNames { get; } = new List<string> {"a", "b"};
public NotEq() : base("notEq", true, public NotEq() : base("notEq", true,
new[] new[]
{ {
Curry.ConstructFrom(Typs.Bool, new Var("a"), new Var("a")), Curry.ConstructFrom(Typs.Bool, new Var("a"), new Var("a")),
Curry.ConstructFrom(Typs.String, new Var("a"), new Var("a")) Curry.ConstructFrom(Typs.String, new Var("a"), new Var("a")),
new Curry(Typs.Bool, Typs.Bool),
}) })
{ {
Funcs.AddBuiltin(this, "not"); Funcs.AddBuiltin(this, "not");
@ -35,6 +37,13 @@ namespace AspectedRouting.Functions
public override object Evaluate(Context c, params IExpression[] arguments) public override object Evaluate(Context c, params IExpression[] arguments)
{ {
if (arguments.Length == 1)
{
var booleanArg = arguments[0].Evaluate(c);
return booleanArg.Equals("no");
}
var arg0 = arguments[0].Evaluate(c); var arg0 = arguments[0].Evaluate(c);
var arg1 = arguments[1].Evaluate(c); var arg1 = arguments[1].Evaluate(c);
if ((!(arg0?.Equals(arg1) ?? false))) if ((!(arg0?.Equals(arg1) ?? false)))

View file

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Parameter : IExpression public class Parameter : IExpression
{ {
@ -25,16 +25,8 @@ namespace AspectedRouting.Functions
public object Evaluate(Context c, params IExpression[] args) public object Evaluate(Context c, params IExpression[] args)
{ {
return c?.Parameters?.GetValueOrDefault(ParamName, null); var paramName = ParamName.TrimStart('#'); // context saves paramnames without '#'
} return c?.Parameters?.GetValueOrDefault(paramName, null);
public void EvaluateAll(Context c, HashSet<IExpression> addTo)
{
var v = Evaluate(c);
if (v != null)
{
addTo.Add(this);
}
} }
public IExpression Specialize(IEnumerable<Type> allowedTypes) public IExpression Specialize(IEnumerable<Type> allowedTypes)

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Parse : Function public class Parse : Function
{ {
@ -13,6 +14,7 @@ namespace AspectedRouting.Functions
new[] new[]
{ {
new Curry(Typs.String, Typs.Double), new Curry(Typs.String, Typs.Double),
new Curry(Typs.String, Typs.PDouble),
}) })
{ {
} }

View file

@ -1,13 +1,18 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
/// <summary> /// <summary>
/// Converts a function 'string -> string -> a' onto a function 'tags -> [a]' /// Converts a function 'string -> string -> a' onto a function 'tags -> [a]'
/// </summary> /// </summary>
public class StringStringToTagsFunction : Function public class StringStringToTagsFunction : Function
{ {
public override string Description { get; } =
"stringToTags converts a function `string -> string -> a` into a function `tags -> [a]`";
public override List<string> ArgNames { get; } = new List<string>{"f","tags"};
private static Type baseFunction = private static Type baseFunction =
Curry.ConstructFrom(new Var("a"), Typs.String, Typs.String); Curry.ConstructFrom(new Var("a"), Typs.String, Typs.String);
@ -15,7 +20,8 @@ namespace AspectedRouting.Functions
public StringStringToTagsFunction() : base("stringToTags", true, public StringStringToTagsFunction() : base("stringToTags", true,
new[] new[]
{ {
new Curry(baseFunction, new Curry(Typs.Tags, new ListType(new Var("a")))) new Curry(baseFunction,
new Curry(Typs.Tags, new ListType(new Var("a"))))
} }
) )
{ {

View file

@ -1,8 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class Sum : Function public class Sum : Function
{ {

View file

@ -1,7 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using AspectedRouting.Typ; using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Functions namespace AspectedRouting.Language.Functions
{ {
public class ToString : Function public class ToString : Function
{ {

View file

@ -1,60 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using AspectedRouting.Functions; using AspectedRouting.Language.Functions;
using AspectedRouting.Typ; using AspectedRouting.Language.Typ;
using Type = AspectedRouting.Typ.Type; using Type = AspectedRouting.Language.Typ.Type;
namespace AspectedRouting namespace AspectedRouting.Language
{ {
public class Context
{
public Dictionary<string, IExpression> Parameters = new Dictionary<string, IExpression>();
public Dictionary<string, AspectMetadata> DefinedFunctions = new Dictionary<string, AspectMetadata>();
public void AddParameter(string name, string value)
{
Parameters.Add(name, new Constant(value));
}
public void AddFunction(string name, AspectMetadata function)
{
if (Funcs.Builtins.ContainsKey(name))
{
throw new ArgumentException("Function " + name + " already exists, it is a builtin function");
}
if (DefinedFunctions.ContainsKey(name))
{
throw new ArgumentException("Function " + name + " already exists");
}
DefinedFunctions.Add(name, function);
}
public IExpression GetFunction(string name)
{
if (name.StartsWith("$"))
{
name = name.Substring(1);
}
if (Funcs.Builtins.ContainsKey(name))
{
return Funcs.Builtins[name];
}
if (DefinedFunctions.ContainsKey(name))
{
return DefinedFunctions[name];
}
throw new ArgumentException(
$"The function {name} is not a defined nor builtin function. Known functions are " +
string.Join(", ", DefinedFunctions.Keys));
}
}
public interface IExpression public interface IExpression
{ {
IEnumerable<Type> Types { get; } IEnumerable<Type> Types { get; }
@ -79,6 +32,11 @@ namespace AspectedRouting
public static class ExpressionExtensions public static class ExpressionExtensions
{ {
public static object Run(this IExpression e, Context c, Dictionary<string, string> tags)
{
return e.Apply(new []{new Constant(tags)}).Evaluate(c);
}
public static IExpression Specialize(this IExpression e, Type t) public static IExpression Specialize(this IExpression e, Type t)
{ {
if (t == null) if (t == null)
@ -125,6 +83,7 @@ namespace AspectedRouting
/// THen specializes every expression onto this common ground /// THen specializes every expression onto this common ground
/// </summary> /// </summary>
/// <returns>The common ground of types</returns> /// <returns>The common ground of types</returns>
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public static IEnumerable<IExpression> SpecializeToCommonTypes(this IEnumerable<IExpression> exprs, public static IEnumerable<IExpression> SpecializeToCommonTypes(this IEnumerable<IExpression> exprs,
out IEnumerable<Type> specializedTypes, out IEnumerable<IExpression> specializedExpressions) out IEnumerable<Type> specializedTypes, out IEnumerable<IExpression> specializedExpressions)
{ {
@ -139,12 +98,13 @@ namespace AspectedRouting
} }
var specialized = specializedTypes.SpecializeTo(f.Types.RenameVars(specializedTypes)); var specialized = specializedTypes.SpecializeTo(f.Types.RenameVars(specializedTypes));
// ReSharper disable once JoinNullCheckWithUsage
if (specialized == null) if (specialized == null)
{ {
throw new ArgumentException("Could not unify\n " throw new ArgumentException("Could not unify\n "
+ "<previous items>: " + string.Join(", ", specializedTypes) + + "<previous items>: " + string.Join(", ", specializedTypes) +
"\nwith\n " "\nwith\n "
+ f + ": " + string.Join(", ", f.Types)); + f.Optimize() + ": " + string.Join(", ", f.Types));
} }
specializedTypes = specialized; specializedTypes = specialized;

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class BoolType : Type public class BoolType : Type
{ {

View file

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class Curry : Type public class Curry : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class DoubleType : Type public class DoubleType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class IntType : Type public class IntType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class ListType : Type public class ListType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class NatType : Type public class NatType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class PDoubleType : Type public class PDoubleType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class StringType : Type public class StringType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class TagsType : Type public class TagsType : Type
{ {

View file

@ -1,4 +1,4 @@
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public abstract class Type public abstract class Type
{ {

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public static class Typs public static class Typs
{ {

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace AspectedRouting.Typ namespace AspectedRouting.Language.Typ
{ {
public class Var : Type public class Var : Type
{ {

View file

@ -1,12 +1,7 @@
-- The different profiles -- The different profiles
profiles = { profiles = {
{
name = "comfort_safety_speed",
description = "A route which aims to be both safe and comfortable without sacrificing too much of speed",
function_name = "determine_weights_balanced",
metric = "custom"
},
{ {
name = "b2w", name = "b2w",
@ -72,151 +67,10 @@ profiles = {
} }
--[[
What is a relative speedup/slowdown for each key?
]]
speed_bonuses = {
access = {
dismount = 0.2
},
highway = {
path = 0.5, -- A small path through a forest is typically very slow to go through
track = 0.7 -- an unmaintained track (in Belgium: tractor path) is slower as well
},
surface = {
paved = 0.99,
asphalt = 1, -- default speed is kept
concrete = 1,
metal = 1,
wood = 1,
["concrete:lanes"] = 0.95,
["concrete:plates"] = 1,
paving_stones = 1,
sett = 0.9, -- sett slows us down with 10%
unhewn_cobblestone = 0.75,
cobblestone = 0.8,
unpaved = 0.75,
compacted = 0.99,
fine_gravel = 0.99,
gravel = 0.9,
dirt = 0.6,
earth = 0.6,
grass = 0.6,
grass_paver = 0.9,
ground = 0.7,
sand = 0.5,
woodchips = 0.5,
snow = 0.5,
pebblestone = 0.5,
mud = 0.4
},
tracktype = {
grade1 = 0.99,
grade2 = 0.8,
grade3 = 0.6,
grade4 = 0.3,
grade5 = 0.1
},
incline = {
up = 0.75,
down = 1.25,
[0] = 1,
["0%"] = 1,
["10%"] = 0.9,
["-10%"] = 1.1,
["20%"] = 0.8,
["-20%"] = 1.2,
["30%"] = 0.7,
["-30%"] = 1.3,
}
}
--[[
Determine-speed determines the average speed for the given segment based on _physical properties_,
such as surface, road incline, ...
It is _not_ concerned with a legal (or socially accepted) speed - this is capped in the calling procedure
]]
function determine_speed(attributes, result)
local factor = calculate_factor(attributes, speed_bonuses, result)
result.speed = factor * attributes.settings.default_speed
return result.speed
end
--[[
How comfortable and nice is this road?
Note: this is quite subjective as well!
Takes a guess on how _comfortable_ this road is to travel.
Here, aspects are:
- road surface and smoothness
- estimated greenery (abandoned=railway for the win)
]]
comfort_bonuses = {
highway = {
cycleway = 1.2,
primary = 0.3,
secondary = 0.4,
tertiary = 0.5,
unclassified = 0.8,
track = 0.95,
residential = 1.0,
living_street = 1.1,
footway = 0.95,
path = 0.5
},
railway = {
abandoned = 2.0
},
cycleway = {
track = 1.2
},
access = {
designated = 1.2,
dismount = 0.5
},
cyclestreet = {
yes = 1.1
},
-- Quite, but not entirely the same as in speed
surface = {
paved = 0.99,
["concrete:lanes"] = 0.8,
["concrete:plates"] = 1,
sett = 0.9, -- sett slows us down with 10%
unhewn_cobblestone = 0.75,
cobblestone = 0.8,
unpaved = 0.75,
compacted = 1.1,
fine_gravel = 0.99,
gravel = 0.9,
dirt = 0.6,
earth = 0.6,
grass = 0.6,
grass_paver = 0.9,
ground = 0.7,
sand = 0.5,
woodchips = 0.5,
snow = 0.5,
pebblestone = 0.5,
mud = 0.4
},
}
function determine_comfort(attributes, result)
return calculate_factor(attributes, comfort_bonuses, result)
end
-- Returns 1 if no access restrictions, 0 if it is permissive -- Returns 1 if no access restrictions, 0 if it is permissive
function determine_permissive_score(attributes, result) function determine_permissive_score(attributes, result)
if (attributes.access == "permissive") then if (attributes.access == "permissive") then
@ -674,30 +528,6 @@ end
function unit_tests() function unit_tests()
-- Speed test are with default_speed = 100, in order to efficiently determine the factor
unit_test_speed({ highway = "residential" }, 100.0)
unit_test_speed({ highway = "residential", surface = "sett" }, 90.0)
unit_test_speed({ highway = "residential", incline = "up" }, 75.0)
unit_test_speed({ highway = "residential", incline = "up", surface = "mud" }, 30.0)
unit_test_speed({ highway = "residential", incline = "down" }, 125.0)
unit_test_speed({ highway = "residential", incline = "down", surface = "sett" }, 112.5)
unit_test_comfort({ highway = "residential" }, 1.0)
unit_test_comfort({ highway = "residential", cyclestreet = "yes" }, 1.1)
unit_test_comfort({ highway = "cycleway" }, 1.2)
unit_test_comfort({ highway = "cycleway", foot = "designated" }, 1.2)
unit_test_comfort({ highway = "path", bicycle = "designated", foot = "designated" }, 0.5)
unit_test_comfort({ highway = "path", bicycle = "designated" }, 0.5)
unit_test_comfort({ highway = "footway", foot = "designated" }, 0.95)
unit_test_comfort({ highway = "primary", cycleway = "no" }, 0.3)
unit_test_comfort({ highway = "primary", cycleway = "yes" }, 0.3)
unit_test_comfort({ highway = "primary", cycleway = "track" }, 0.36)
unit_test_comfort({ highway = "secondary", cycleway = "lane" }, 0.4)
unit_test_comfort({ ["cycleway:right"] = "lane", highway = "secondary", surface = "asphalt" }, 0.4)
unit_test_comfort({ highway = "residential", surface = "asphalt", cyclestreet = "yes" }, 1.1)
unit_test_relation_tag_processor({ route = "bicycle", operator = "Stad Genk", color = "red", type = "route" }, unit_test_relation_tag_processor({ route = "bicycle", operator = "Stad Genk", color = "red", type = "route" },
{ StadGenk = "yes", cycle_network_colour = "red", cycle_network = "yes" }); { StadGenk = "yes", cycle_network_colour = "red", cycle_network = "yes" });

View file

@ -0,0 +1,4 @@
access,oneway,speed,priority,highway,_relation:bicycle.network_by_operator
no,both,0,0,,
yes,both,15,1.9,residential,
yes,both,15,21.9,residential,yes
1 access oneway speed priority highway _relation:bicycle.network_by_operator
2 no both 0 0
3 yes both 15 1.9 residential
4 yes both 15 21.9 residential yes

View file

@ -1,12 +1,60 @@
{ {
"name": "bicycle.comfort", "name": "bicycle.comfort",
"description": "Gives a comfort factor, purely based on physical aspects of the road", "description": "Gives a comfort factor for a road, purely based on physical aspects of the road, which is a bit subjective; this takes a bit of scnery into account with a preference for `railway=abandoned` and `towpath=yes`",
"unit": "[0, 2]", "unit": "[0, 2]",
"$default": 1, "$default": 1,
"value": { "value": {
"$multiply": { "$multiply": {
"highway": {
"cycleway": 1.2,
"primary": 0.3,
"secondary": 0.4,
"tertiary": 0.5,
"unclassified": 0.8,
"track": 0.95,
"residential": 1.0,
"living_street": 1.1,
"footway": 0.95,
"path": 0.5
},
"railway": {
"abandoned": 2
},
"towpath": {
"yes": 2
},
"cycleway": {
"track": 1.2
},
"cyclestreet": { "cyclestreet": {
"yes": 1.5 "yes": 1.1
},
"access": {
"designated": 1.2,
"dismount": 0.5
},
"surface": {
"#": "The surface mapping heavily resembles the one in speed_factor, but it is not entirely the same",
"paved": 0.99,
"concrete:lanes": 0.8,
"concrete:plates": 1.0,
"sett": 0.9,
"unhewn_cobblestone": 0.75,
"cobblestone": 0.8,
"unpaved": 0.75,
"compacted": 1.1,
"fine_gravel": 0.99,
"gravel": 0.9,
"dirt": 0.6,
"earth": 0.6,
"grass": 0.6,
"grass_paver": 0.9,
"ground": 0.7,
"sand": 0.5,
"woodchips": 0.5,
"snow": 0.5,
"pebblestone": 0.5,
"mud": 0.4
} }
} }
} }

View file

@ -0,0 +1,18 @@
expected,highway,foot,bicycle,cyclestreet,cycleway,cycleway:right,surface,railway,towpath
1,,,,,,,,,
1,residential,,,,,,,,
1.1,residential,,,yes,,,,,
1.2,cycleway,,,,,,,,
1.2,cycleway,designated,,,,,,,
0.5,path,designated,designated,,,,,,
0.5,path,,designated,,,,,,
0.95,footway,designated,,,,,,,
0.3,primary,,,,no,,,,
0.3,primary,,,,yes,,,,
0.36,primary,,,,track,,,,
0.4,secondary,,,,lane,,,,
0.4,secondary,,,,,lane,asphalt,,
1.1,residential,,,yes,,,asphalt,,
2,,,,,,,,abandoned,
2,,,,,,,,,yes
4,,,,,,,,abandoned,yes
1 expected highway foot bicycle cyclestreet cycleway cycleway:right surface railway towpath
2 1
3 1 residential
4 1.1 residential yes
5 1.2 cycleway
6 1.2 cycleway designated
7 0.5 path designated designated
8 0.5 path designated
9 0.95 footway designated
10 0.3 primary no
11 0.3 primary yes
12 0.36 primary track
13 0.4 secondary lane
14 0.4 secondary lane asphalt
15 1.1 residential yes asphalt
16 2 abandoned
17 2 yes
18 4 abandoned yes

View file

@ -0,0 +1,4 @@
access,oneway,speed,priority,highway,_relation:bicycle.network_score
designated,both,15,22,cycleway,yes
no,,,,,
designated,both,15,2,cycleway,
1 access oneway speed priority highway _relation:bicycle.network_score
2 designated both 15 22 cycleway yes
3 no
4 designated both 15 2 cycleway

View file

@ -1,7 +1,7 @@
access,oneway,speed,weight,highway,bicycle access,oneway,speed,priority,highway,bicycle
0,0,0,0,, no,both,0,0,,
1,0,30,0.03333,cycleway, designated,both,15,15,cycleway,
0,0,0,0,primary, no,both,0,0,primary,
1,0,15,0.06666,pedestrian, dismount,both,2.25,2.25,pedestrian,
1,0,15,0.0666,pedestrian,yes yes,both,15,15,pedestrian,yes
1,0,30,0.033333,residential, yes,both,15,15,residential,

1 access oneway speed weight priority highway bicycle
2 0 no 0 both 0 0 0
3 1 designated 0 both 30 15 0.03333 15 cycleway
4 0 no 0 both 0 0 0 primary
5 1 dismount 0 both 15 2.25 0.06666 2.25 pedestrian
6 1 yes 0 both 15 0.0666 15 pedestrian yes
7 1 yes 0 both 30 15 0.033333 15 residential

View file

@ -16,25 +16,27 @@
"network" "network"
], ],
"defaults": { "defaults": {
"#max_speed": 30,
"#defaultSpeed": 15, "#defaultSpeed": 15,
"#speed": 0, "#maxspeed": 30,
"#timeNeeded": 0,
"#distance": 0, "#distance": 0,
"#comfort": 0, "#comfort": 0,
"#safety": 0, "#safety": 0,
"#withDirection": "yes", "#network": 0,
"#againstDirection": "yes", "#networkOperator": []
"network": 0
}, },
"profiles": { "behaviours": {
"fastest": { "fastest": {
"description": "The fastest route to your destination", "description": "The fastest route to your destination",
"#time_needed": 1 "#timeNeeded": 1
}, },
"shortest": { "shortest": {
"description": "The shortest route, independent of of speed", "description": "The shortest route, independent of of speed",
"#distance": 1 "#distance": 1
}, },
"safety": { "safety": {
"description": "A defensive route shying away from big roads with lots of cars", "description": "A defensive route shying away from big roads with lots of cars",
"#safety": 1 "#safety": 1
@ -48,10 +50,12 @@
"#comfort": 1, "#comfort": 1,
"#safety": 1 "#safety": 1
}, },
"node_networks": {
"description": "A route which prefers cycling over cycling node networks. Non-network parts prefer some comfort and safety. The on-network-part is considered flat (meaning that only distance on the node network counts)",
"brussels": {
"description": "A route preferring the cycling network by operator 'Brussels Mobility'",
"#network": 20, "#network": 20,
"#network-node-network-only":true, "#networkOperator": ["Brussels Mobility"],
"#comfort": 1, "#comfort": 1,
"#safety": 1 "#safety": 1
} }
@ -61,16 +65,20 @@
"speed": { "speed": {
"$min": [ "$min": [
"$legal_maxspeed_be", "$legal_maxspeed_be",
"#defaultSpeed" "#maxspeed",
{
"$multiply": [
"#defaultSpeed",
"$bicycle.speed_factor"
]
}
] ]
}, },
"weight": { "priority": {
"#comfort": "$bicycle.comfort", "#comfort": "$bicycle.comfort",
"#safety": "$bicycle.safety", "#safety": "$bicycle.safety",
"#network": "$bicycle.network_score", "#network": "$bicycle.network_by_operator",
"#time_needed": { "#timeNeeded": "$speed",
"$inv": "$speed"
},
"#distance": "$distance" "#distance": "$distance"
} }
} }

View file

@ -0,0 +1,13 @@
{
"name": "bicycle.network_by_operator",
"description": "The 'bicycle.network_score' returns true if the way is part of a cycling network of a certain (group of) operators",
"$memberOf": {
"$mustMatch": {
"type": "route",
"route": "bicycle",
"state": {"$notEq": "proposed"},
"operator": {"$containedIn": "#networkOperator"}
}
}
}

View file

@ -1,9 +1,12 @@
{ {
"name": "bicycle.network_score", "name": "bicycle.network_score",
"description": "The 'bicycle.network_score' is a bit of a catch-all for bicycle networks and indicates wether or not the road is part of a matching cycling network.", "description": "The 'bicycle.network_score' returns true if the way is part of a cycling network",
"$mustMatch": { "$memberOf": {
"type": "route", "$mustMatch": {
"route": "bicycle" "type": "route",
"route": "bicycle",
"state": {"$notEq": "proposed"}
}
} }
} }

View file

@ -0,0 +1,62 @@
{
"name": "bicycle.speed_factor",
"description": "Calculates a speed factor for bicycles based on physical features, e.g. a sand surface will slow a cyclist down; going over pedestrian areas even more, ...",
"$multiply": {
"access": {
"#": "We have to go by foot. Default speed of 20km/h * 0.15 = 3km/h",
"dismount": 0.15
},
"highway": {
"#": "A small forest path is typically slow",
"path": 0.5,
"#": "an unmaintained track (in Belgium: tractor path) is slower as well",
"track": 0.7
},
"surface": {
"paved": 0.99,
"asphalt": 1,
"concrete": 1,
"metal": 1,
"wood": 1,
"concrete:lanes": 0.95,
"concrete:plates": 1,
"paving_stones": 1,
"sett": 0.9,
"unhewn_cobblestone": 0.75,
"cobblestone": 0.8,
"unpaved": 0.75,
"compacted": 0.99,
"fine_gravel": 0.99,
"gravel": 0.9,
"dirt": 0.6,
"earth": 0.6,
"grass": 0.6,
"grass_paver": 0.9,
"ground": 0.7,
"sand": 0.5,
"woodchips": 0.5,
"snow": 0.5,
"pebblestone": 0.5,
"mud": 0.4
},
"tracktype": {
"grade1": 0.99,
"grade2": 0.8,
"grade3": 0.6,
"grade4": 0.3,
"grade5": 0.1
},
"incline": {
"up": 0.75,
"down": 1.25,
"0": 1,
"0%": 1,
"10%": 0.9,
"-10%": 1.1,
"20%": 0.8,
"-20%": 1.2,
"30%": 0.7,
"-30%": 1.3
}
}
}

View file

@ -0,0 +1,12 @@
expected,incline,surface,highway,access
1,,,,
1,,,residential,
0.75,up,,residential,
1.25,down,,residential,
0.3,up,mud,residential,
1.125,down,sett,residential,
0.675,up,sett,residential,
0.9,,sett,residential,
1,,asphalt,residential,
0.15,,,residential,dismount
0.0315,up,mud,track,dismount
1 expected incline surface highway access
2 1
3 1 residential
4 0.75 up residential
5 1.25 down residential
6 0.3 up mud residential
7 1.125 down sett residential
8 0.675 up sett residential
9 0.9 sett residential
10 1 asphalt residential
11 0.15 residential dismount
12 0.0315 up mud track dismount

View file

@ -2,110 +2,132 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using AspectedRouting.Functions;
using AspectedRouting.IO; using AspectedRouting.IO;
using AspectedRouting.IO.itinero1;
using AspectedRouting.IO.jsonParser;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Tests;
namespace AspectedRouting namespace AspectedRouting
{ {
class Program static class Program
{ {
public static List<(AspectMetadata aspect, FunctionTestSuite tests)> ParseAspects(
this List<string> jsonFileNames, Context context)
{
var aspects = new List<(AspectMetadata aspect, FunctionTestSuite tests)>();
foreach (var file in jsonFileNames)
{
var fi = new FileInfo(file);
var aspect = JsonParser.AspectFromJson(context, File.ReadAllText(file), fi.Name);
if (aspect != null)
{
var testPath = fi.DirectoryName + "/" + aspect.Name + ".test.csv";
FunctionTestSuite tests = null;
if (File.Exists(testPath))
{
tests = FunctionTestSuite.FromString(aspect, File.ReadAllText(testPath));
}
aspects.Add((aspect, tests));
}
}
return aspects;
}
private static LuaPrinter GenerateLua(Context context,
List<(AspectMetadata aspect, FunctionTestSuite tests)> aspects,
ProfileMetaData profile, List<ProfileTestSuite> profileTests)
{
var luaPrinter = new LuaPrinter(context);
foreach (var (aspect, tests) in aspects)
{
luaPrinter.AddFunction(aspect);
if (tests != null)
{
luaPrinter.AddTestSuite(tests);
}
}
luaPrinter.AddProfile(profile);
foreach (var testSuite in profileTests)
{
luaPrinter.AddTestSuite(testSuite);
}
return luaPrinter;
}
private static (ProfileMetaData profile, List<ProfileTestSuite> profileTests) ParseProfile(string profilePath,
Context context)
{
var profile = JsonParser.ProfileFromJson(context, File.ReadAllText(profilePath), new FileInfo(profilePath));
profile.SanityCheckProfile(context);
var profileFi = new FileInfo(profilePath);
var profileTests = new List<ProfileTestSuite>();
foreach (var behaviourName in profile.Behaviours.Keys)
{
var testPath = profileFi.DirectoryName + "/" + profile.Name + "." + behaviourName + ".csv";
if (File.Exists(testPath))
{
var test = ProfileTestSuite.FromString(context, profile, behaviourName, File.ReadAllText(testPath));
profileTests.Add(test);
}
else
{
Console.WriteLine($"[{behaviourName}] WARNING: no test found for behaviour");
}
}
return (profile, profileTests);
}
public static void Main(string[] args) public static void Main(string[] args)
{ {
MdPrinter.GenerateHelpText("IO/md/helpText.md");
var files = Directory.EnumerateFiles("Profiles", "*.json", SearchOption.AllDirectories) var files = Directory.EnumerateFiles("Profiles", "*.json", SearchOption.AllDirectories)
.ToList(); .ToList();
var context = new Context(); var context = new Context();
var aspects = ParseAspects(files, context);
MdPrinter.GenerateHelpText("IO/md/helpText.md"); foreach (var (aspect, t) in aspects)
var testSuites = new List<FunctionTestSuite>();
var aspects = new List<AspectMetadata>();
foreach (var file in files)
{ {
var fi = new FileInfo(file); if (t == null)
var aspect = JsonParser.AspectFromJson(context, File.ReadAllText(file), fi.Name);
if (aspect != null)
{ {
aspects.Add(aspect); Console.WriteLine($"[{aspect.Name}] WARNING: no tests found: please add {aspect.Name}.test.csv");
}
var testPath = fi.DirectoryName + "/" + aspect.Name + ".test.csv"; else
if (File.Exists(testPath)) {
{ t.Run();
var tests = FunctionTestSuite.FromString(aspect, File.ReadAllText(testPath));
testSuites.Add(tests);
}
else
{
Console.WriteLine($"[{aspect.Name}] No tests found: to directory" );
}
} }
}
foreach (var aspect in aspects)
{
context.AddFunction(aspect.Name, aspect); context.AddFunction(aspect.Name, aspect);
} }
var profilePath = "Profiles/bicycle/bicycle.json"; var profilePath = "Profiles/bicycle/bicycle.json";
var profile = JsonParser.ProfileFromJson(context, File.ReadAllText(profilePath), new FileInfo(profilePath)); var (profile, profileTests) = ParseProfile(profilePath, context);
var profileFi = new FileInfo(profilePath); foreach (var test in profileTests)
var profileTests = new List<ProfileTestSuite>();
foreach (var profileName in profile.Profiles.Keys)
{ {
var testPath = profileFi.DirectoryName + "/" + profile.Name + "." + profileName + ".csv"; test.Run(context);
if (File.Exists(testPath))
{
var test = ProfileTestSuite.FromString(profile, profileName, File.ReadAllText(testPath));
profileTests.Add(test);
}
}
profile.SanityCheckProfile();
var luaPrinter = new LuaPrinter();
luaPrinter.CreateProfile(profile, context);
var testCode = "\n\n\n\n\n\n\n\n--------------------------- Test code -------------------------\n\n\n";
foreach (var testSuite in testSuites)
{
testSuite.Run();
testCode += testSuite.ToLua() + "\n";
}
foreach (var testSuite in profileTests)
{
testCode += testSuite.ToLua() + "\n";
} }
// Compatibility code, itinero-transit doesn't know 'print' var luaPrinter = GenerateLua(context, aspects, profile, profileTests);
testCode += string.Join("\n",
"",
"if (itinero == nil) then",
" itinero = {}",
" itinero.log = print",
"",
" -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended",
" runTests = true",
"",
"",
"else",
" print = itinero.log",
"end",
"",
"if (not failed_tests and not failed_profile_tests) then",
" print(\"Tests OK\")",
"end"
);
File.WriteAllText("output.lua", luaPrinter.ToLua() + testCode); File.WriteAllText("output.lua", luaPrinter.ToLua());
} }
} }
} }

View file

@ -1,14 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AspectedRouting.Functions; using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
namespace AspectedRouting.IO namespace AspectedRouting.Tests
{ {
public class FunctionTestSuite public class FunctionTestSuite
{ {
private readonly AspectMetadata _functionToApply; public readonly AspectMetadata FunctionToApply;
private readonly IEnumerable<(string expected, Dictionary<string, string> tags)> _tests; public readonly IEnumerable<(string expected, Dictionary<string, string> tags)> Tests;
public static FunctionTestSuite FromString(AspectMetadata function, string csvContents) public static FunctionTestSuite FromString(AspectMetadata function, string csvContents)
{ {
@ -47,47 +49,23 @@ namespace AspectedRouting.IO
AspectMetadata functionToApply, AspectMetadata functionToApply,
IEnumerable<(string expected, Dictionary<string, string> tags)> tests) IEnumerable<(string expected, Dictionary<string, string> tags)> tests)
{ {
_functionToApply = functionToApply;
_tests = tests;
}
if (functionToApply == null)
public string ToLua()
{
var tests = string.Join("\n", _tests.Select((test, i) => ToLua(i, test.expected, test.tags)));
return "\n" + tests + "\n";
}
private string ToLua(int index, string expected, Dictionary<string, string> tags)
{
var parameters = new Dictionary<string, string>();
foreach (var (key, value) in tags)
{ {
if (key.StartsWith("#")) throw new NullReferenceException("functionToApply is null");
{
parameters[key.TrimStart('#')] = value;
}
} }
FunctionToApply = functionToApply;
foreach (var (paramName, _) in parameters) Tests = tests;
{
tags.Remove("#" + paramName);
}
var funcName = _functionToApply.Name.Replace(" ", "_").Replace(".", "_");
return
$"unit_test({funcName}, \"{_functionToApply.Name}\", {index}, \"{expected}\", {parameters.ToLuaTable()}, {tags.ToLuaTable()})";
} }
public void Run() public void Run()
{ {
var failed = false; var failed = false;
var testCase = 0; var testCase = 0;
foreach (var test in _tests) foreach (var test in Tests)
{ {
testCase++; testCase++;
var context = new Context(); var context = new Context();
@ -99,14 +77,14 @@ namespace AspectedRouting.IO
} }
} }
var actual = _functionToApply.Evaluate(context, new Constant(test.tags)); var actual = FunctionToApply.Evaluate(context, new Constant(test.tags));
if (!actual.ToString().Equals(test.expected) && if (!actual.ToString().Equals(test.expected) &&
!(actual is double actualD && Math.Abs(double.Parse(test.expected) - actualD) < 0.0001) !(actual is double actualD && Math.Abs(double.Parse(test.expected) - actualD) < 0.0001)
) )
{ {
failed = true; failed = true;
Console.WriteLine( Console.WriteLine(
$"[{_functionToApply.Name}] Testcase {testCase} failed:\n Expected: {test.expected}\n actual: {actual}\n tags: {test.tags.Pretty()}"); $"[{FunctionToApply.Name}] Testcase {testCase} failed:\n Expected: {test.expected}\n actual: {actual}\n tags: {test.tags.Pretty()}");
} }
} }
@ -115,7 +93,7 @@ namespace AspectedRouting.IO
throw new ArgumentException("Some test failed"); throw new ArgumentException("Some test failed");
} }
Console.WriteLine($"[{_functionToApply.Name}] {testCase} tests successful"); Console.WriteLine($"[{FunctionToApply.Name}] {testCase} tests successful");
} }
} }
} }

View file

@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AspectedRouting.Language;
using AspectedRouting.Language.Expression;
using AspectedRouting.Language.Functions;
using AspectedRouting.Language.Typ;
namespace AspectedRouting.Tests
{
public struct Expected
{
public readonly string Access;
public readonly string Oneway;
public readonly double Speed;
public readonly double Weight;
public Expected(string access, string oneway, double speed, double weight)
{
Access = access;
Oneway = oneway;
Speed = speed;
Weight = weight;
}
}
public class ProfileTestSuite
{
public readonly ProfileMetaData Profile;
public readonly string BehaviourName;
public readonly IEnumerable<(Expected, Dictionary<string, string> tags)> Tests;
public static ProfileTestSuite FromString(Context c, ProfileMetaData function, string profileName,
string csvContents)
{
try
{
var all = csvContents.Split("\n").ToList();
var keys = all[0].Split(",").ToList();
keys = keys.GetRange(4, keys.Count - 4).Select(k => k.Trim()).ToList();
foreach (var k in keys)
{
if (k.StartsWith("_relations:"))
{
throw new ArgumentException(
"To inject relation memberships, use '_relation:<aspect_name>', without S after relation");
}
if (k.StartsWith("_relation:"))
{
var aspectName = k.Substring("_relation:".Length);
if (aspectName.Contains(":"))
{
throw new ArgumentException(
"To inject relation memberships, use '_relation:<aspect_name>', don't add the behaviour name");
}
if (!c.DefinedFunctions.ContainsKey(aspectName))
{
throw new ArgumentException(
$"'_relation:<aspect_name>' detected, but the aspect {aspectName} wasn't found. Try one of: " +
string.Join(",", c.DefinedFunctions.Keys));
}
}
}
var tests = new List<(Expected, Dictionary<string, string>)>();
var line = 1;
foreach (var test in all.GetRange(1, all.Count - 1))
{
line++;
if (string.IsNullOrEmpty(test.Trim()))
{
continue;
}
try
{
var testData = test.Split(",").ToList();
var speed = 0.0;
if (!string.IsNullOrEmpty(testData[2]))
{
speed = double.Parse(testData[2]);
}
var weight = 0.0;
if (!string.IsNullOrEmpty(testData[3]))
{
weight = double.Parse(testData[3]);
}
var expected = new Expected(
testData[0],
testData[1],
speed,
weight
);
var vals = testData.GetRange(4, testData.Count - 4);
var tags = new Dictionary<string, string>();
for (int i = 0; i < keys.Count; i++)
{
if (i < vals.Count && !string.IsNullOrEmpty(vals[i]))
{
tags[keys[i]] = vals[i];
}
}
tests.Add((expected, tags));
}
catch (Exception e)
{
throw new Exception("On line " + line, e);
}
}
return new ProfileTestSuite(function, profileName, tests);
}
catch (Exception e)
{
throw new Exception("In the profile test file for " + profileName, e);
}
}
public ProfileTestSuite(
ProfileMetaData profile,
string profileName,
IEnumerable<(Expected, Dictionary<string, string> tags)> tests)
{
Profile = profile;
BehaviourName = profileName;
Tests = tests;
}
private static bool Eq(Context c, string value, object result)
{
var v = Funcs.Eq.Apply(new Constant(value), new Constant(Typs.String, result));
var o = v.Evaluate(c);
return o is string s && s.Equals("yes");
}
public bool RunTest(Context c, int i, Expected expected, Dictionary<string, string> tags)
{
c = new Context(c);
tags = new Dictionary<string, string>(tags);
void Err(string message, object exp, object act)
{
Console.WriteLine(
$"[{Profile.Name}.{BehaviourName}]: Test {i} failed: {message}; expected {exp} but got {act}\n {{{tags.Pretty()}}}");
}
var success = true;
var canAccess = Profile.Access.Run(c, tags);
tags["access"] = "" + canAccess;
if (!expected.Access.Equals(canAccess))
{
Err("access value incorrect", expected.Access, canAccess);
success = false;
}
if (expected.Access.Equals("no"))
{
return success;
}
var oneway = Profile.Oneway.Run(c, tags);
tags["oneway"] = "" + oneway;
if (!Eq(c, expected.Oneway, oneway))
{
Err("oneway value incorrect", expected.Oneway, oneway);
success = false;
}
var speed = (double) Profile.Speed.Run(c, tags);
tags["speed"] = "" + speed;
c.AddFunction("speed", new AspectMetadata(new Constant(Typs.Double, speed),
"speed", "Actual speed of this function", "NA","NA","NA", true));
c.AddFunction("oneway", new AspectMetadata(new Constant(Typs.String, oneway),
"oneway", "Actual direction of this function", "NA","NA","NA", true));
c.AddFunction("access", new AspectMetadata(new Constant(Typs.String, canAccess),
"access", "Actual access of this function", "NA","NA","NA", true));
if (Math.Abs(speed - expected.Speed) > 0.0001)
{
Err("speed value incorrect", expected.Speed, speed);
success = false;
}
var priority = 0.0;
foreach (var (paramName, expression) in Profile.Priority)
{
var aspectInfluence = (double) c.Parameters[paramName].Evaluate(c);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (aspectInfluence == 0)
{
continue;
}
var aspectWeightObj = new Apply(
Funcs.EitherFunc.Apply(Funcs.Id, Funcs.Const, expression)
, new Constant((tags))).Evaluate(c);
double aspectWeight;
switch (aspectWeightObj)
{
case bool b:
aspectWeight = b ? 1.0 : 0.0;
break;
case double d:
aspectWeight = d;
break;
case int j:
aspectWeight = j;
break;
case string s:
if (s.Equals("yes"))
{
aspectWeight = 1.0;
break;
}
else if (s.Equals("no"))
{
aspectWeight = 0.0;
break;
}
throw new Exception($"Invalid value as result for {paramName}: got string {s}");
default:
throw new Exception($"Invalid value as result for {paramName}: got object {aspectWeightObj}");
}
priority += aspectInfluence * aspectWeight;
}
if (Math.Abs(priority - expected.Weight) > 0.0001)
{
Err("weight incorrect", expected.Weight, priority);
success = false;
}
return success;
}
public void Run(Context c)
{
var parameters = new Dictionary<string, IExpression>();
foreach (var (k, v) in Profile.DefaultParameters)
{
parameters[k] = v;
}
foreach (var (k, v) in Profile.Behaviours[BehaviourName])
{
parameters[k] = v;
}
c = c.WithParameters(parameters);
var allOk = true;
var i = 1;
foreach (var (expected, tags) in Tests)
{
try
{
allOk &= RunTest(c, i, expected, tags);
}
catch (Exception e)
{
throw new Exception("In a test for " + BehaviourName, e);
}
i++;
}
if (!allOk)
{
throw new ArgumentException("Some tests failed for " + BehaviourName);
}
else
{
Console.WriteLine($"[{BehaviourName}] {Tests.Count()} tests successful");
}
}
}
}

View file

@ -19,5 +19,7 @@ namespace AspectedRouting
return factor; return factor;
} }
} }
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff