Skip to content

Commit

Permalink
[Expression] Add formatNumber, formatEpoch and formatTicks as prebuil…
Browse files Browse the repository at this point in the history
…t functions (#3876) (#3883)

* add formatNumber prebuilt function

* change to string output to keep tailing zeors

* Add formatEpoch and formatTicks.
Removed ticks from formatDate and fixed some bugs.
Fixed a bug in JSON conversion where 64 bit/double values were getting truncated.

* Add tests that were dropped.

* Add locale to formatNumber.

* Add 1000's seperators.

* fix a test case

* change a test case

Co-authored-by: Chris McConnell <[email protected]>

Co-authored-by: Chris McConnell <[email protected]>
  • Loading branch information
cosmicshuai and chrimc62 authored May 7, 2020
1 parent e53da83 commit 0a2e35c
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 51 deletions.
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

0 comments on commit 0a2e35c

Please sign in to comment.