Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cherry-pick] formatNumber, formatEpoch and formatTicks as prebuilt fumctions #3883

Merged
merged 1 commit into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 99 additions & 22 deletions libraries/AdaptiveExpressions/ExpressionFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using AdaptiveExpressions.Memory;
using AdaptiveExpressions.Properties;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -927,7 +925,7 @@ public static (object result, string error) SetProperty(object instance, string
/// <summary>
/// Convert constant JValue to base type value.
/// </summary>
/// <param name="obj">input object.</param>
/// <param name="obj">Input object.</param>
/// <returns>Corresponding base type if input is a JValue.</returns>
public static object ResolveValue(object obj)
{
Expand All @@ -941,7 +939,7 @@ public static object ResolveValue(object obj)
value = jval.Value;
if (jval.Type == JTokenType.Integer)
{
value = jval.ToObject<int>();
value = jval.ToObject<long>();
}
else if (jval.Type == JTokenType.String)
{
Expand All @@ -953,7 +951,7 @@ public static object ResolveValue(object obj)
}
else if (jval.Type == JTokenType.Float)
{
value = jval.ToObject<float>();
value = jval.ToObject<double>();
}
}

Expand Down Expand Up @@ -3371,29 +3369,70 @@ private static IDictionary<string, ExpressionEvaluator> GetStandardFunctions()
{
object result = null;
string error = null;
object timestamp = args[0];
if (Extensions.IsNumber(timestamp))
{
if (double.TryParse(args[0].ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out double unixTimestamp))
{
var dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
timestamp = dateTime.AddSeconds(unixTimestamp);
}
}

var timestamp = args[0];
if (timestamp is string tsString)
{
(result, error) = ParseTimestamp(tsString, dt => dt.ToString(args.Count() == 2 ? args[1].ToString() : DefaultDateTimeFormat, CultureInfo.InvariantCulture));
}
else if (timestamp is DateTime dt)
{
result = dt.ToString(args.Count() == 2 ? args[1].ToString() : DefaultDateTimeFormat, CultureInfo.InvariantCulture);
}
else
{
(result, error) = ParseTimestamp((string)((DateTime)timestamp).ToString(CultureInfo.InvariantCulture), dt => dt.ToString(args.Count() == 2 ? args[1].ToString() : DefaultDateTimeFormat, CultureInfo.InvariantCulture));
error = $"formatDateTime has invalid first argument {timestamp}";
}

return (result, error);
}),
ReturnType.String,
(expr) => ValidateOrder(expr, new[] { ReturnType.String }, ReturnType.Object)),
new ExpressionEvaluator(
ExpressionType.FormatEpoch,
ApplyWithError(
args =>
{
object result = null;
string error = null;
var timestamp = args[0];
if (timestamp.IsNumber())
{
var dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dateTime = dateTime.AddSeconds(Convert.ToDouble(timestamp));
result = dateTime.ToString(args.Count() == 2 ? args[1].ToString() : DefaultDateTimeFormat, CultureInfo.InvariantCulture);
}
else
{
error = $"formatEpoch first argument {timestamp} is not a number";
}

return (result, error);
}),
ReturnType.String,
(expr) => ValidateOrder(expr, new[] { ReturnType.String }, ReturnType.Number)),
new ExpressionEvaluator(
ExpressionType.FormatTicks,
ApplyWithError(
args =>
{
object result = null;
string error = null;
var timestamp = args[0];
if (timestamp.IsInteger())
{
var ticks = Convert.ToInt64(timestamp);
var dateTime = new DateTime(ticks);
result = dateTime.ToString(args.Count() == 2 ? args[1].ToString() : DefaultDateTimeFormat, CultureInfo.InvariantCulture);
}
else
{
error = $"formatTicks first arugment {timestamp} must be an integer";
}

return (result, error);
}),
ReturnType.String,
(expr) => ValidateOrder(expr, new[] { ReturnType.String }, ReturnType.Number)),
new ExpressionEvaluator(
ExpressionType.SubtractFromTime,
(expr, state, options) =>
Expand Down Expand Up @@ -3761,7 +3800,7 @@ private static IDictionary<string, ExpressionEvaluator> GetStandardFunctions()
{
(parsed, error) = ParseTimexProperty(args[0]);
}

if (error == null)
{
value = parsed.Hour != null && parsed.Minute != null && parsed.Second != null;
Expand Down Expand Up @@ -4072,6 +4111,44 @@ private static IDictionary<string, ExpressionEvaluator> GetStandardFunctions()
new ExpressionEvaluator(ExpressionType.String, Apply(args => JsonConvert.SerializeObject(args[0]).TrimStart('"').TrimEnd('"')), ReturnType.String, ValidateUnary),
Comparison(ExpressionType.Bool, args => IsLogicTrue(args[0]), ValidateUnary),
new ExpressionEvaluator(ExpressionType.Xml, ApplyWithError(args => ToXml(args[0])), ReturnType.String, ValidateUnary),
new ExpressionEvaluator(
ExpressionType.FormatNumber,
ApplyWithError(
args =>
{
string result = null;
string error = null;
if (!args[0].IsNumber())
{
error = $"formatNumber first argument ${args[0]} must be number";
}
else if (!args[1].IsInteger())
{
error = $"formatNumber second argument ${args[1]} must be number";
}
else if (args.Count == 3 && !(args[2] is string))
{
error = $"formatNumber third agument ${args[2]} must be a locale";
}
else
{
try
{
var number = Convert.ToDouble(args[0]);
var precision = Convert.ToInt32(args[1]);
var locale = args.Count == 3 ? new CultureInfo(args[2] as string) : CultureInfo.InvariantCulture;
result = number.ToString("N" + precision.ToString(), locale);
}
catch
{
error = $"{args[3]} is not a valid locale for formatNumber";
}
}

return (result, error);
}),
ReturnType.String,
(expr) => ValidateOrder(expr, new[] { ReturnType.String }, ReturnType.Number, ReturnType.Number)),

// Misc
new ExpressionEvaluator(ExpressionType.Accessor, Accessor, ReturnType.Object, ValidateAccessor),
Expand Down Expand Up @@ -4156,7 +4233,7 @@ private static IDictionary<string, ExpressionEvaluator> GetStandardFunctions()
return JToken.ReadFrom(jsonReader);
}
}
}),
}),
ReturnType.Object,
(expr) => ValidateOrder(expr, null, ReturnType.String)),
new ExpressionEvaluator(
Expand Down Expand Up @@ -4271,9 +4348,9 @@ private static IDictionary<string, ExpressionEvaluator> GetStandardFunctions()
new ExpressionEvaluator(
ExpressionType.IsDateTime,
Apply(
args =>
args =>
{
if (args[0] is string)
if (args[0] is string)
{
object value = null;
string error = null;
Expand All @@ -4293,7 +4370,7 @@ private static IDictionary<string, ExpressionEvaluator> GetStandardFunctions()
var eval = new ExpressionEvaluator(ExpressionType.Optional, (expression, state, options) => throw new NotImplementedException(), ReturnType.Boolean, ValidateUnaryBoolean);
eval.Negation = eval;
functions.Add(eval);

eval = new ExpressionEvaluator(ExpressionType.Ignore, (expression, state, options) => expression.Children[0].TryEvaluate(state, options), ReturnType.Boolean, ValidateUnaryBoolean);
eval.Negation = eval;
functions.Add(eval);
Expand Down
3 changes: 3 additions & 0 deletions libraries/AdaptiveExpressions/ExpressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public static class ExpressionType
public const string Year = "year";
public const string UtcNow = "utcNow";
public const string FormatDateTime = "formatDateTime";
public const string FormatEpoch = "formatEpoch";
public const string FormatTicks = "formatTicks";
public const string SubtractFromTime = "subtractFromTime";
public const string DateReadBack = "dateReadBack";
public const string GetTimeOfDay = "getTimeOfDay";
Expand Down Expand Up @@ -124,6 +126,7 @@ public static class ExpressionType
public const string UriComponent = "uriComponent";
public const string UriComponentToString = "uriComponentToString";
public const string Xml = "xml";
public const string FormatNumber = "formatNumber";

// URI Parsing Functions
public const string UriHost = "uriHost";
Expand Down
36 changes: 23 additions & 13 deletions tests/AdaptiveExpressions.Tests/BadExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public class BadExpressionTests
Test("base64ToBinary(one)"), // should have string param
Test("base64ToString(hello, world)"), // shoule have 1 param
Test("base64ToString(false)"), // should have string param
Test("formatNumber(1,2,3)"), // invalid locale type
Test("formatNumber(1,2,'dlkj'"), // invalid locale
Test("formatNumber(1,2.0)"), // the second parameter should be an integer
Test("formatNumber(hello,2.0)"), // the first parameter should be a number
#endregion

#region Math functions test
Expand Down Expand Up @@ -228,7 +232,13 @@ public class BadExpressionTests
Test("formatDateTime(notValidTimestamp)"), // error datetime format
Test("formatDateTime(notValidTimestamp2)"), // error datetime format
Test("formatDateTime(notValidTimestamp3)"), // error datetime format
Test("formatDateTime(timestamp, 'yyyy', 1)"), // should have 2 or 3 params
Test("formatDateTime({})"), // error valid datetime
Test("formatDateTime(timestamp, 1)"), // invalid format string
Test("formatEpoch('time')"), // error string
Test("formatEpoch(timestamp, 'yyyy', 1)"), // should have 1 or 2 params
Test("formatTicks('string')"), // String is not valid
Test("formatTicks(2.3)"), // float is not valid
Test("formatTicks({})"), // object is not valid
Test("subtractFromTime('errortime', 'yyyy', 1)"), // error datetime format
Test("subtractFromTime(timestamp, 1, 'W')"), // error time unit
Test("subtractFromTime(timestamp, timestamp, 'W')"), // error parameters format
Expand Down Expand Up @@ -338,7 +348,7 @@ public class BadExpressionTests
Test("sortBy(createArray('H','e','l','l','o'), 'x', hi)"), // second param should be string
#endregion

#region Object manipulation and construction functions test
#region Object manipulation and construction functions test
Test("json(1,2)"), // should have 1 parameter
Test("json(1)"), // should be string parameter
Test("json('{\"key1\":value1\"}')"), // invalid json format string
Expand All @@ -356,44 +366,44 @@ public class BadExpressionTests
Test("jPath(hello,'Manufacturers[0].Products[0].Price')"), // not a valid json
Test("jPath(hello,'Manufacturers[0]/Products[0]/Price')"), // not a valid path
Test("jPath(jsonStr,'$..Products[?(@.Price >= 100)].Name')"), // no matched node
#endregion
#endregion

#region Memory access test
#region Memory access test
Test("getProperty(bag, 1)"), // second param should be string
Test("Accessor(1)"), // first param should be string
Test("Accessor(bag, 1)"), // second should be object
Test("one[0]"), // one is not list
Test("items[3]"), // index out of range
Test("items[one+0.5]"), // index is not integer
#endregion
#endregion

#region Regex
#region Regex
Test("isMatch('^[a-z]+$')"), // should have 2 parameter
Test("isMatch('abC', one)"), // second param should be string
Test("isMatch(1, '^[a-z]+$')"), // first param should be string
Test("isMatch('abC', '^[a-z+$')"), // bad regular expression
#endregion
#endregion

#region Type Checking
#region Type Checking
Test("isString(hello, hello)"), // should have 1 parameter
Test("isInteger(2, 3)"), // should have 1 parameter
Test("isFloat(1.2, 3.1)"), // should have 1 parameter
Test("isArray(createArray(1,2,3), 1)"), // should have 1 parameter
Test("isObejct(emptyJObject, hello)"), // should have 1 parameter
Test("isDateTime('2018-03-15T13:00:00.000Z', hello)"), // should have 1 parameter
Test("isBoolean(false, false)"), // should have 1 parameter
#endregion
#endregion

#region SetPathToValue tests
#region SetPathToValue tests
Test("setPathToValue(2+3, 4)"), // Not a real path
Test("setPathToValue(a)"), // Missing value
#endregion
#endregion

#region TriggerTree Tests
#region TriggerTree Tests

// optional throws because it's a placeholder only interpreted by trigger tree and is removed before evaluation
Test("optional(true)"),
#endregion
#endregion
};

/// <summary>
Expand Down
Loading