diff --git a/src/OneScript.Core/Values/BslDateValue.cs b/src/OneScript.Core/Values/BslDateValue.cs index 91ac459a0..50863cadf 100644 --- a/src/OneScript.Core/Values/BslDateValue.cs +++ b/src/OneScript.Core/Values/BslDateValue.cs @@ -14,6 +14,8 @@ namespace OneScript.Values { public sealed class BslDateValue : BslPrimitiveValue, IBslComparable { + private const long TicksPerStep = TimeSpan.TicksPerSecond / 10000; // 1000 + private readonly DateTime _value; private BslDateValue(DateTime value) @@ -21,7 +23,23 @@ private BslDateValue(DateTime value) _value = value; } - public static BslDateValue Create(DateTime value) => new BslDateValue(value); + public static DateTime Normalize(DateTime value) + { + var ticks = value.Ticks; + var rounded = (long)Math.Round((double)ticks / TicksPerStep, MidpointRounding.AwayFromZero) * TicksPerStep; + return new DateTime(rounded, value.Kind); + } + + public static BslDateValue Create(DateTime value) => new BslDateValue(Normalize(value)); + + public static DateTime AddSeconds(DateTime date, decimal seconds) + { + var rounded = Math.Round((double)seconds, 4, MidpointRounding.AwayFromZero); + return Normalize(date.AddTicks((long)(rounded * TimeSpan.TicksPerSecond))); + } + + public static DateTime SubtractSeconds(DateTime date, decimal seconds) => + AddSeconds(date, -seconds); public override int CompareTo(BslValue other) { @@ -67,15 +85,11 @@ public override string ToString() #region Date Arithmetics - public static DateTime operator +(BslDateValue left, decimal right) - { - return left._value.AddSeconds((double) right); - } + public static DateTime operator +(BslDateValue left, decimal right) => + AddSeconds(left._value, right); - public static DateTime operator -(BslDateValue left, decimal right) - { - return left._value.AddSeconds(-(double) right); - } + public static DateTime operator -(BslDateValue left, decimal right) => + SubtractSeconds(left._value, right); public static decimal operator -(BslDateValue left, DateTime right) { diff --git a/src/OneScript.Native/Compiler/DateOperations.cs b/src/OneScript.Native/Compiler/DateOperations.cs index 4627a2d2f..523192adc 100644 --- a/src/OneScript.Native/Compiler/DateOperations.cs +++ b/src/OneScript.Native/Compiler/DateOperations.cs @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the using System; using System.Diagnostics; using System.Linq.Expressions; +using OneScript.Values; namespace OneScript.Native.Compiler { @@ -26,18 +27,12 @@ public static Expression DateOffsetOperation(Expression left, Expression right, Debug.Assert(left.Type == typeof(DateTime)); Debug.Assert(right.Type == typeof(decimal)); - var adder = typeof(DateTime).GetMethod(nameof(DateTime.AddSeconds)); - Debug.Assert(adder != null); - - var toDouble = Expression.Convert(right, typeof(double)); - Expression arg = opCode switch - { - ExpressionType.Add => toDouble, - ExpressionType.Subtract => Expression.Negate(toDouble), - _ => throw NativeCompilerException.OperationNotDefined(opCode, left.Type, right.Type) - }; + var method = opCode == ExpressionType.Add + ? typeof(BslDateValue).GetMethod(nameof(BslDateValue.AddSeconds), new[] { typeof(DateTime), typeof(decimal) }) + : typeof(BslDateValue).GetMethod(nameof(BslDateValue.SubtractSeconds), new[] { typeof(DateTime), typeof(decimal) }); + Debug.Assert(method != null); - return Expression.Call(left, adder, arg); + return Expression.Call(method, left, right); } /// diff --git a/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs b/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs index 338da894a..508cba9d5 100644 --- a/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs +++ b/src/OneScript.StandardLibrary/Json/GlobalJsonFunctions.cs @@ -13,6 +13,9 @@ This Source Code Form is subject to the terms of the using OneScript.Exceptions; using OneScript.Execution; using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Timezones; +using OneScript.Types; +using OneScript.Values; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -42,9 +45,9 @@ public static IAttachableContext CreateInstance() /// Если установлено Ложь, объекты будут считываться в объект типа Структура. /// Значение по умолчанию: Ложь. /// - /// Значение не обрабатывается в текущей версии. Значение по умолчанию: Неопределено. + /// Имена свойств JSON, значения которых нужно преобразовать в дату. Значение по умолчанию: Неопределено. /// - /// Значение не обрабатывается в текущей версии. Значение по умолчанию: ISO. + /// Формат даты во входных строках. Значение по умолчанию: ISO. /// /// Значение не обрабатывается в текущей версии. Значение по умолчанию: Неопределено. /// @@ -59,15 +62,50 @@ public static IAttachableContext CreateInstance() /// /// [ContextMethod("ПрочитатьJSON", "ReadJSON")] - public IValue ReadJSON(JSONReader Reader, bool ReadToMap = false, IValue PropertiesWithDateValuesNames = null, IValue ExpectedDateFormat = null, string ReviverFunctionName = null, IValue ReviverFunctionModule = null, IValue ReviverFunctionAdditionalParameters = null, IValue RetriverPropertiesNames = null, int MaximumNesting = 500) + public IValue ReadJSON(JSONReader Reader, bool ReadToMap = false, IValue PropertiesWithDateValuesNames = null, JSONDateFormatEnum? ExpectedDateFormat = null, string ReviverFunctionName = null, IValue ReviverFunctionModule = null, IValue ReviverFunctionAdditionalParameters = null, IValue RetriverPropertiesNames = null, int MaximumNesting = 500) { - var jsonReader = new JsonReaderInternal(Reader); + var dateFormat = ExpectedDateFormat ?? JSONDateFormatEnum.ISO; + if (dateFormat == JSONDateFormatEnum.JavaScript) + { + throw new RuntimeException(Locale.NStr( + "ru='Формат даты JavaScript не поддерживается.'; en='JavaScript date format is not supported'")); + } + + var datePropertyNames = ParseDatePropertyNames(PropertiesWithDateValuesNames); + var jsonReader = new JsonReaderInternal(Reader, datePropertyNames, dateFormat); return ReadToMap ? jsonReader.Read() : jsonReader.Read(); } + private static HashSet ParseDatePropertyNames(IValue namesValue) + { + if (namesValue == null || namesValue.SystemType == BasicTypes.Undefined) + return new HashSet(StringComparer.Ordinal); + + if (namesValue.SystemType == BasicTypes.String) + return new HashSet(StringComparer.Ordinal) { namesValue.AsString(ForbiddenBslProcess.Instance) }; + + if (namesValue is IValueArray array) + { + var result = new HashSet(StringComparer.Ordinal); + foreach (var item in array) + { + if (item.SystemType != BasicTypes.String) + throw RuntimeException.InvalidArgumentType(); + + result.Add(item.AsString(ForbiddenBslProcess.Instance)); + } + + return result; + } + + throw RuntimeException.InvalidArgumentType(); + } + internal class JsonReaderInternal { private readonly JSONReader _reader; + private readonly HashSet _datePropertyNames; + private readonly JSONDateFormatEnum _dateFormat; private Func _builder; private Action _inserter; @@ -89,9 +127,11 @@ private void Init() } } - public JsonReaderInternal(JSONReader reader) + public JsonReaderInternal(JSONReader reader, HashSet datePropertyNames, JSONDateFormatEnum dateFormat) { _reader = reader; + _datePropertyNames = datePropertyNames; + _dateFormat = dateFormat; } private IValue Create() => _builder(); @@ -112,6 +152,10 @@ public IValue Read() where T: IEnumerable { throw; } + catch (RuntimeException) + { + throw; + } catch (Exception exc) { throw InvalidJsonException(exc.Message); @@ -145,6 +189,13 @@ private bool ReadJsonValue(out IValue value) if (!ReadJsonValue(out value)) return false; + if (_datePropertyNames.Count > 0 + && _datePropertyNames.Contains(propertyName) + && value is BslStringValue stringValue) + { + value = ParseJsonDate(stringValue.ToString(), _dateFormat); + } + AddProperty(jsonObject, propertyName, value); } @@ -196,7 +247,10 @@ private RuntimeException InvalidJsonException(string message=null) public IValue ReadJSONInMap(JSONReader reader) { - var jsonReader = new JsonReaderInternal(reader); + var jsonReader = new JsonReaderInternal( + reader, + new HashSet(StringComparer.Ordinal), + JSONDateFormatEnum.ISO); return jsonReader.Read(); } @@ -215,13 +269,17 @@ public IValue ReadJSONInMap(JSONReader reader) /// [ContextMethod("ПрочитатьДатуJSON", "ReadJSONDate")] public IValue ReadJSONDate(string String, JSONDateFormatEnum? format) + { + return ParseJsonDate(String, format ?? JSONDateFormatEnum.ISO); + } + + private static IValue ParseJsonDate(string dateString, JSONDateFormatEnum format) { DateFormatHandling dateFormatHandling; switch (format) { case JSONDateFormatEnum.ISO: - case null: dateFormatHandling = DateFormatHandling.IsoDateFormat; break; case JSONDateFormatEnum.Microsoft: @@ -233,22 +291,29 @@ public IValue ReadJSONDate(string String, JSONDateFormatEnum? format) "ru='Формат даты JavaScript не поддерживается.'; en='JavaScript date format is not supported'")); } - string json = @"{""Date"":""" + String + @"""}"; + string json = @"{""Date"":""" + dateString + @"""}"; var settings = new JsonSerializerSettings { DateFormatHandling = dateFormatHandling }; + DateTime dateTime; try { var result = JsonConvert.DeserializeObject(json, settings); - return ValueFactory.Create((DateTime)result.Date); + dateTime = (DateTime)result.Date; } catch (JsonException) { throw new RuntimeException(Locale.NStr("ru='Представление даты имеет неверный формат.'; en='Invalid date presentation format'")); } + + if (dateTime.Kind == DateTimeKind.Utc) + dateTime = TimeZoneConverter.ToLocalTime(dateTime); + + dateTime = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second); + return ValueFactory.Create(dateTime); } /// diff --git a/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs b/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs index fec6bea55..05205595a 100644 --- a/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs +++ b/src/OneScript.StandardLibrary/Json/JSONDateWriter.cs @@ -16,10 +16,14 @@ namespace OneScript.StandardLibrary.Json { internal static class JSONDateWriter { + internal static string FormatDateForJson(DateTime date) + { + return FormatISODate(DropSubsecond(date)); + } + public static string Write(IValue dateValue, JSONDateFormatEnum format, JSONDateWritingVariantEnum dateWritingVariant) { - var date = GetDateArgument(dateValue); - date = DropSubsecond(date); + var date = DropSubsecond(GetDateArgument(dateValue)); switch (format) { diff --git a/src/OneScript.StandardLibrary/Json/JSONWriter.cs b/src/OneScript.StandardLibrary/Json/JSONWriter.cs index b85d72a43..509ba8ec4 100644 --- a/src/OneScript.StandardLibrary/Json/JSONWriter.cs +++ b/src/OneScript.StandardLibrary/Json/JSONWriter.cs @@ -271,7 +271,7 @@ public void WriteValue(IValue value, bool useFormatWithExponent = false) _writer.WriteValue(v); break; case DateTime v: - _writer.WriteValue(v); + _writer.WriteValue(JSONDateWriter.FormatDateForJson(v)); break; case null: diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index f12754f12..0d236e7ed 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -100,7 +100,9 @@ public string DetailErrorDescription(ExceptionInfoContext errInfo) [ContextMethod("ТекущаяУниверсальнаяДата", "CurrentUniversalDate")] public IValue CurrentUniversalDate() { - return ValueFactory.Create(DateTime.UtcNow); + var date = DateTime.UtcNow; + date = date.AddTicks(-(date.Ticks % TimeSpan.TicksPerSecond)); + return ValueFactory.Create(date); } [ContextMethod("ТекущаяУниверсальнаяДатаВМиллисекундах", "CurrentUniversalDateInMilliseconds")] diff --git a/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs b/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs index 97533cd58..4a6ee7cb6 100644 --- a/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs +++ b/src/OneScript.StandardLibrary/Xml/XmlGlobalFunctions.cs @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Xml; using OneScript.Contexts; using OneScript.Exceptions; @@ -65,7 +66,10 @@ public string XMLString(BslValue value) else if(value.SystemType == BasicTypes.Boolean) return XmlConvert.ToString(value.AsBoolean()); else if(value.SystemType == BasicTypes.Date) - return XmlConvert.ToString(value.AsDate(), XmlDateTimeSerializationMode.Unspecified); + { + var date = value.AsDate(); + return date.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture); + } else if(value.SystemType == BasicTypes.Number) return XmlConvert.ToString(value.AsNumber()); else diff --git a/src/Tests/OneScript.Core.Tests/DateValueCompatibilityTests.cs b/src/Tests/OneScript.Core.Tests/DateValueCompatibilityTests.cs new file mode 100644 index 000000000..44c59509b --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/DateValueCompatibilityTests.cs @@ -0,0 +1,344 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OneScript.Commons; +using OneScript.Contexts; +using OneScript.Execution; +using OneScript.Native.Runtime; +using OneScript.StandardLibrary; +using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Json; +using OneScript.StandardLibrary.Timezones; +using OneScript.StandardLibrary.Xml; +using OneScript.Values; +using ScriptEngine; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class DateValueCompatibilityTests + { + private static readonly BslDateValue Base = + (BslDateValue)ValueFactory.Create(new DateTime(2026, 2, 1, 15, 30, 45)); + + private const string JsonText = + "{\"a\":\"2026-02-01T15:30:45\"," + + "\"b\":\"2026-02-01T15:30:45.123\"," + + "\"c\":\"2026-02-01T15:30:45Z\"," + + "\"d\":\"2026-02-01T15:30:45+03:00\"," + + "\"e\":\"2026-02-01T15:30:45.1234567Z\"}"; + + private static DateTime DropFraction(DateTime dt) => + new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second); + + private static IValue GetStructureProperty(StructureImpl structure, string name) => + structure.GetPropValue(structure.GetPropertyNumber(name)); + + public static IEnumerable FractionalSubtractionCases => new[] + { + new object[] { 0.5m, 0.5m }, + new object[] { 1.5m, 1.5m }, + new object[] { 0.25m, 0.25m }, + new object[] { 2m, 2m }, + }; + + private static JObject ParseJson(string json) + { + using (var reader = new JsonTextReader(new StringReader(json)) + { + DateParseHandling = DateParseHandling.None + }) + { + return JObject.Load(reader); + } + } + + private static string WriteStructureToJson(StructureImpl structure) + { + var writer = new JSONWriter(); + writer.SetString(); + new GlobalJsonFunctions().WriteJSON(ForbiddenBslProcess.Instance, writer, structure); + return writer.Close(); + } + + [Theory] + [InlineData(0.001)] + [InlineData(0.1)] + [InlineData(0.5)] + [InlineData(0.999)] + [InlineData(0.0001)] + public void Date_Addition_Above_1C_Precision_Changes_Value(double delta) + { + var result = ValueFactory.Add(Base, ValueFactory.Create((decimal)delta), ForbiddenBslProcess.Instance); + + Assert.False(Base.Equals(result)); + } + + [Theory] + [InlineData(0.00001)] + [InlineData(0.0000001)] + [InlineData(0.00004)] + public void Date_Addition_Below_Half_Of_1C_Precision_Is_Ignored(double delta) + { + var result = ValueFactory.Add(Base, ValueFactory.Create((decimal)delta), ForbiddenBslProcess.Instance); + + Assert.True(Base.Equals(result)); + } + + [Theory] + [InlineData(0.00005, 0.0001)] + [InlineData(0.00006, 0.0001)] + [InlineData(0.00016, 0.0002)] + [InlineData(0.00025, 0.0003)] + public void Date_Addition_Is_Rounded_To_1C_Precision(double addition, double expectedShift) + { + var result = ValueFactory.Add(Base, ValueFactory.Create((decimal)addition), ForbiddenBslProcess.Instance); + var expectedDelta = (decimal)expectedShift; + + Assert.Equal(expectedDelta, ValueFactory.Sub(result, Base).AsNumber()); + + var expectedDate = ValueFactory.Add(Base, ValueFactory.Create(expectedDelta), ForbiddenBslProcess.Instance); + Assert.True(result.Equals(expectedDate)); + } + + [Fact] + public void Date_Fractional_Addition_Is_Not_Rounded_To_Whole_Second() + { + var plus06 = ValueFactory.Add(Base, ValueFactory.Create(0.6m), ForbiddenBslProcess.Instance); + var plus1 = ValueFactory.Add(Base, ValueFactory.Create(1m), ForbiddenBslProcess.Instance); + var plus04 = ValueFactory.Add(Base, ValueFactory.Create(0.4m), ForbiddenBslProcess.Instance); + var plus0004 = ValueFactory.Add(Base, ValueFactory.Create(0.0004m), ForbiddenBslProcess.Instance); + var plus0006 = ValueFactory.Add(Base, ValueFactory.Create(0.0006m), ForbiddenBslProcess.Instance); + + Assert.False(Base.Equals(plus06)); + Assert.False(plus1.Equals(plus06)); + Assert.False(Base.Equals(plus04)); + Assert.False(Base.Equals(plus0004)); + Assert.False(Base.Equals(plus0006)); + } + + [Theory] + [MemberData(nameof(FractionalSubtractionCases))] + public void Date_Subtraction_Returns_Fractional_Seconds(decimal delta, decimal expectedDiff) + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(delta), ForbiddenBslProcess.Instance); + var diff = ValueFactory.Sub(shifted, Base).AsNumber(); + + Assert.Equal(expectedDiff, diff); + } + + [Fact] + public void Date_Subtraction_Result_Is_Not_Integer() + { + var diff = ValueFactory.Sub( + ValueFactory.Add(Base, ValueFactory.Create(0.5m), ForbiddenBslProcess.Instance), + Base).AsNumber(); + + Assert.NotEqual(decimal.Truncate(diff), diff); + } + + [Fact] + public void Date_Comparison_Distinguishes_Fractions() + { + var fractional = ValueFactory.Add(Base, ValueFactory.Create(0.001m), ForbiddenBslProcess.Instance); + + Assert.True(fractional.CompareTo(Base) > 0); + Assert.True(fractional.CompareTo(Base) >= 0); + Assert.False(fractional.CompareTo(Base) < 0); + Assert.False(Base.Equals(fractional)); + Assert.NotEqual(fractional, Base); + } + + [Fact] + public void CurrentDate_Has_No_Fractional_Seconds() + { + var current = BuiltInFunctions.CurrentDate(); + + Assert.Equal(DropFraction(current), current); + } + + [Fact] + public void CurrentUniversalDate_Has_No_Fractional_Seconds() + { + var universal = (DateTime)(BslDateValue)new StandardGlobalContext().CurrentUniversalDate(); + + Assert.Equal(DropFraction(universal), universal); + } + + [Fact] + public void ToLocalTime_Of_Whole_Second_Date_Has_No_Fractional_Seconds() + { + var utc = DropFraction(DateTime.UtcNow); + var local = TimeZoneConverter.ToLocalTime(utc); + + Assert.Equal(DropFraction(local), local); + } + + [Fact] + public void Timezone_Roundtrip_Preserves_Fractional_Seconds() + { + var frac = (BslDateValue)ValueFactory.Add(Base, ValueFactory.Create(0.001m), ForbiddenBslProcess.Instance); + var utc = TimeZoneConverter.ToUniversalTime((DateTime)frac); + var back = TimeZoneConverter.ToLocalTime(utc); + + Assert.True(frac.Equals((BslValue)ValueFactory.Create(back))); + } + + [Fact] + public void Date_ToString_Hides_Fractional_Seconds() + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance); + + Assert.Equal(Base.ToString(), shifted.ToString()); + } + + [Fact] + public void Format_Ignores_Fractional_Seconds() + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance); + + Assert.Equal("45", ValueFormatter.Format((BslValue)shifted, "ДФ=сс")); + Assert.Equal("20260201153045", ValueFormatter.Format((BslValue)shifted, "ДФ=ггггММддЧЧммсс")); + } + + [Fact] + public void XmlString_Drops_Fractional_Seconds() + { + var shifted = ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance); + var mgr = new Mock(); + mgr.Setup(m => m.GetInstance(It.IsAny())).Returns((IAttachableContext)null); + var xml = (XmlGlobalFunctions)XmlGlobalFunctions.CreateInstance(mgr.Object); + + Assert.Equal("2026-02-01T15:30:45", xml.XMLString((BslValue)shifted)); + } + + [Fact] + public void Json_Write_Naive_Date_Uses_Iso_Seconds() + { + var structure = new StructureImpl(); + structure.Insert("Литерал", ValueFactory.Create(new DateTime(2026, 2, 1, 15, 30, 45))); + structure.Insert("ПустаяДата", ValueFactory.Create(DateTime.MinValue)); + + var json = WriteStructureToJson(structure); + var obj = ParseJson(json); + + Assert.Equal("2026-02-01T15:30:45", obj["Литерал"]?.ToString()); + Assert.Equal("0001-01-01T00:00:00", obj["ПустаяДата"]?.ToString()); + } + + [Fact] + public void Json_Write_Drops_Fractional_Seconds() + { + var structure = new StructureImpl(); + structure.Insert("Дробная", ValueFactory.Add(Base, ValueFactory.Create(0.123m), ForbiddenBslProcess.Instance)); + + var json = WriteStructureToJson(structure); + var obj = ParseJson(json); + + Assert.Equal("2026-02-01T15:30:45", obj["Дробная"]?.ToString()); + } + + [Fact] + public void Json_Write_Ignores_DateTime_Kind() + { + var structure = new StructureImpl(); + structure.Insert("Local", ValueFactory.Create(DropFraction(DateTime.Now))); + structure.Insert("Utc", ValueFactory.Create(DropFraction(DateTime.UtcNow))); + + var json = WriteStructureToJson(structure); + var obj = ParseJson(json); + var pattern = new Regex(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$"); + + var localValue = obj["Local"]?.ToString(); + var utcValue = obj["Utc"]?.ToString(); + + Assert.Matches(pattern, localValue); + Assert.Matches(pattern, utcValue); + Assert.DoesNotContain("Z", utcValue); + Assert.DoesNotContain("+", utcValue); + Assert.DoesNotContain("-", utcValue.Substring(10)); + } + + [Fact] + public void Json_Read_Without_Date_Property_Names_Keeps_Strings() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON(reader); + + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + Assert.IsType(GetStructureProperty(data, "e")); + + Assert.Equal("2026-02-01T15:30:45", GetStructureProperty(data, "a").ToString()); + Assert.Equal("2026-02-01T15:30:45.123", GetStructureProperty(data, "b").ToString()); + Assert.Equal("2026-02-01T15:30:45Z", GetStructureProperty(data, "c").ToString()); + Assert.Equal("2026-02-01T15:30:45+03:00", GetStructureProperty(data, "d").ToString()); + Assert.Equal("2026-02-01T15:30:45.1234567Z", GetStructureProperty(data, "e").ToString()); + } + + [Fact] + public void Json_Read_With_Date_Property_Names_Parses_Iso() + { + var names = new ArrayImpl(); + names.Add(ValueFactory.Create("a")); + names.Add(ValueFactory.Create("b")); + names.Add(ValueFactory.Create("c")); + names.Add(ValueFactory.Create("d")); + names.Add(ValueFactory.Create("e")); + + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + ReadToMap: false, + PropertiesWithDateValuesNames: names, + ExpectedDateFormat: null); + + var expectedLocalFromUtc = TimeZoneInfo.ConvertTimeFromUtc( + new DateTime(2026, 2, 1, 15, 30, 45, DateTimeKind.Utc), + TimeZoneInfo.Local); + var expectedLocalFromUtcD = TimeZoneInfo.ConvertTimeFromUtc( + new DateTime(2026, 2, 1, 12, 30, 45, DateTimeKind.Utc), + TimeZoneInfo.Local); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "b"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "c"), expectedLocalFromUtc); + AssertDateWithoutFraction(GetStructureProperty(data, "d"), expectedLocalFromUtcD); + AssertDateWithoutFraction(GetStructureProperty(data, "e"), expectedLocalFromUtc); + } + + [Fact] + public void ReadJsonDate_Drops_Fractional_Seconds() + { + var result = new GlobalJsonFunctions().ReadJSONDate("2026-02-01T15:30:45.123", JSONDateFormatEnum.ISO); + var date = (DateTime)(BslDateValue)result; + + Assert.Equal(new DateTime(2026, 2, 1, 15, 30, 45), date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + + private static void AssertDateWithoutFraction(IValue value, DateTime expected) + { + Assert.IsType(value); + var date = (DateTime)(BslDateValue)value; + Assert.Equal(expected, date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + } +} diff --git a/src/Tests/OneScript.StandardLibrary.Tests/JsonReadDatePropertiesTests.cs b/src/Tests/OneScript.StandardLibrary.Tests/JsonReadDatePropertiesTests.cs new file mode 100644 index 000000000..9f3e60789 --- /dev/null +++ b/src/Tests/OneScript.StandardLibrary.Tests/JsonReadDatePropertiesTests.cs @@ -0,0 +1,281 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using OneScript.Commons; +using OneScript.Exceptions; +using OneScript.Localization; +using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Json; +using OneScript.Values; +using ScriptEngine.Machine; +using Xunit; + +namespace OneScript.StandardLibrary.Tests +{ + public class JsonReadDatePropertiesTests + { + private const string JsonText = + "{\"a\":\"2026-02-01T15:30:45\"," + + "\"b\":\"2026-02-01T15:30:45.123\"," + + "\"c\":\"2026-02-01T15:30:45Z\"," + + "\"d\":\"2026-02-01T15:30:45+03:00\"," + + "\"e\":\"2026-02-01T15:30:45.1234567Z\"}"; + + private static DateTime LocalOf(int h, int m, int s) => + TimeZoneInfo.ConvertTimeFromUtc( + new DateTime(2026, 2, 1, h, m, s, DateTimeKind.Utc), + TimeZoneInfo.Local); + + private static IValue GetStructureProperty(StructureImpl structure, string name) => + structure.GetPropValue(structure.GetPropertyNumber(name)); + + private static IValue GetMapProperty(MapImpl map, string name) => + map.GetIndexedValue(ValueFactory.Create(name)); + + private static void AssertDateWithoutFraction(IValue value, DateTime expected) + { + Assert.IsType(value); + var date = (DateTime)(BslDateValue)value; + Assert.Equal(expected, date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + + private static ArrayImpl CreateNameArray(params string[] names) + { + var array = new ArrayImpl(); + foreach (var name in names) + array.Add(ValueFactory.Create(name)); + return array; + } + + [Fact] + public void ReadJson_Without_Date_Property_Names_Keeps_Strings() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON(reader); + + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + Assert.IsType(GetStructureProperty(data, "e")); + + Assert.Equal("2026-02-01T15:30:45", GetStructureProperty(data, "a").ToString()); + Assert.Equal("2026-02-01T15:30:45.123", GetStructureProperty(data, "b").ToString()); + Assert.Equal("2026-02-01T15:30:45Z", GetStructureProperty(data, "c").ToString()); + Assert.Equal("2026-02-01T15:30:45+03:00", GetStructureProperty(data, "d").ToString()); + Assert.Equal("2026-02-01T15:30:45.1234567Z", GetStructureProperty(data, "e").ToString()); + } + + [Fact] + public void ReadJson_With_Single_String_Name_Parses_Only_That_Property() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: ValueFactory.Create("a")); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + Assert.IsType(GetStructureProperty(data, "e")); + } + + [Fact] + public void ReadJson_With_ArrayImpl_Names_Parses_Iso_Dates() + { + var names = CreateNameArray("a", "b", "c", "d", "e"); + + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names, + ExpectedDateFormat: JSONDateFormatEnum.ISO); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "b"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "c"), LocalOf(15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "d"), LocalOf(12, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "e"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_With_FixedArrayImpl_Names_Parses_Iso_Dates() + { + var names = new FixedArrayImpl(CreateNameArray("a", "b", "c", "d", "e")); + + var reader = new JSONReader(); + reader.SetString(JsonText); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names, + ExpectedDateFormat: JSONDateFormatEnum.ISO); + + AssertDateWithoutFraction(GetStructureProperty(data, "a"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "b"), new DateTime(2026, 2, 1, 15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "c"), LocalOf(15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "d"), LocalOf(12, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(data, "e"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_With_Names_Parses_Dates_At_Any_Nesting_Level() + { + const string nestedJson = + "{\"outer\":{\"a\":\"2026-02-01T15:30:45Z\"}," + + "\"items\":[{\"b\":\"2026-02-01T15:30:45Z\"}]}"; + + var names = CreateNameArray("a", "b"); + + var reader = new JSONReader(); + reader.SetString(nestedJson); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names); + + var outer = (StructureImpl)GetStructureProperty(data, "outer"); + var items = (ArrayImpl)GetStructureProperty(data, "items"); + var item = (StructureImpl)items.GetIndexedValue(ValueFactory.Create(0)); + + AssertDateWithoutFraction(GetStructureProperty(outer, "a"), LocalOf(15, 30, 45)); + AssertDateWithoutFraction(GetStructureProperty(item, "b"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_ToMap_With_Names_Parses_Dates() + { + var names = CreateNameArray("a"); + + var reader = new JSONReader(); + reader.SetString("{\"a\":\"2026-02-01T15:30:45Z\"}"); + var data = (MapImpl)new GlobalJsonFunctions().ReadJSON( + reader, + ReadToMap: true, + PropertiesWithDateValuesNames: names); + + AssertDateWithoutFraction(GetMapProperty(data, "a"), LocalOf(15, 30, 45)); + } + + [Fact] + public void ReadJson_With_Names_Does_Not_Convert_NonString_Values() + { + const string json = + "{\"a\":123,\"b\":null,\"c\":{\"x\":1},\"d\":[1,2]}"; + + var names = CreateNameArray("a", "b", "c", "d", "missing"); + + var reader = new JSONReader(); + reader.SetString(json); + var data = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: names); + + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + Assert.IsType(GetStructureProperty(data, "c")); + Assert.IsType(GetStructureProperty(data, "d")); + } + + [Fact] + public void ReadJsonDate_Drops_Fractional_Seconds_For_Iso() + { + var result = new GlobalJsonFunctions().ReadJSONDate("2026-02-01T15:30:45.123", JSONDateFormatEnum.ISO); + var date = (DateTime)(BslDateValue)result; + + Assert.Equal(new DateTime(2026, 2, 1, 15, 30, 45), date); + Assert.Equal(0, date.Ticks % TimeSpan.TicksPerSecond); + } + + [Fact] + public void ReadJsonDate_Converts_Z_And_Offset_To_Local() + { + var fromZ = (DateTime)(BslDateValue)new GlobalJsonFunctions().ReadJSONDate( + "2026-02-01T15:30:45Z", + JSONDateFormatEnum.ISO); + var fromOffset = (DateTime)(BslDateValue)new GlobalJsonFunctions().ReadJSONDate( + "2026-02-01T15:30:45+03:00", + JSONDateFormatEnum.ISO); + + Assert.Equal(LocalOf(15, 30, 45), fromZ); + Assert.Equal(LocalOf(12, 30, 45), fromOffset); + } + + [Fact] + public void ReadJson_With_Invalid_Date_String_Throws_RuntimeException() + { + var names = CreateNameArray("a"); + + var reader = new JSONReader(); + reader.SetString("{\"a\":\"not-a-date\"}"); + + var exception = Assert.Throws(() => + new GlobalJsonFunctions().ReadJSON(reader, PropertiesWithDateValuesNames: names)); + + Assert.True(exception.ErrorDescription.BilingualEquals( + "Представление даты имеет неверный формат.", + "Invalid date presentation format")); + } + + [Fact] + public void ReadJsonDate_With_Invalid_String_Throws_RuntimeException() + { + var exception = Assert.Throws(() => + new GlobalJsonFunctions().ReadJSONDate("not-a-date", JSONDateFormatEnum.ISO)); + + Assert.True(exception.ErrorDescription.BilingualEquals( + "Представление даты имеет неверный формат.", + "Invalid date presentation format")); + } + + [Fact] + public void ReadJson_With_JavaScript_Format_Throws() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + + Assert.Throws(() => + new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: CreateNameArray("a"), + ExpectedDateFormat: JSONDateFormatEnum.JavaScript)); + } + + [Fact] + public void ReadJsonDate_With_JavaScript_Format_Throws() + { + Assert.Throws(() => + new GlobalJsonFunctions().ReadJSONDate("2026-02-01T15:30:45", JSONDateFormatEnum.JavaScript)); + } + + [Fact] + public void ReadJson_With_Empty_Names_Keeps_Strings() + { + var reader = new JSONReader(); + reader.SetString(JsonText); + + var withEmptyArray = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: new ArrayImpl()); + + reader.SetString(JsonText); + var withNull = (StructureImpl)new GlobalJsonFunctions().ReadJSON( + reader, + PropertiesWithDateValuesNames: null); + + foreach (var data in new[] { withEmptyArray, withNull }) + { + Assert.IsType(GetStructureProperty(data, "a")); + Assert.IsType(GetStructureProperty(data, "b")); + } + } + } +} diff --git a/src/oscommon.targets b/src/oscommon.targets index 39d0e4266..2fc560484 100644 --- a/src/oscommon.targets +++ b/src/oscommon.targets @@ -6,7 +6,7 @@ 0 Release x86 - default + 12 net8.0 $(NoWarn);CS1591 False diff --git a/tests/date-behavior.os b/tests/date-behavior.os new file mode 100644 index 000000000..4d5b2461f --- /dev/null +++ b/tests/date-behavior.os @@ -0,0 +1,337 @@ +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрибавкаВышеТочности1СМеняетЗначение"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрибавкаНижеПоловиныШага1СИгнорируется"); + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрибавкаОкругляетсяДоШага1С"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ДробнаяЧастьНеОкругляетсяДоСекунды"); + ВсеТесты.Добавить("Тест_ДолженПроверить_РазностьДатВозвращаетДробныеСекунды"); + ВсеТесты.Добавить("Тест_ДолженПроверить_РазностьДатНеЦелая"); + ВсеТесты.Добавить("Тест_ДолженПроверить_СравнениеРазличаетДробнуюЧасть"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ТекущаяДатаБезДолейСекунды"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ТекущаяУниверсальнаяДатаБезДолейСекунды"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_МестноеВремяБезДолейСекунды"); + ВсеТесты.Добавить("Тест_ДолженПроверить_КонвертацияПоясаСохраняетДробь"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_СтрокаСкрываетДробнуюЧасть"); + ВсеТесты.Добавить("Тест_ДолженПроверить_ФорматИгнорируетДробнуюЧасть"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_XMLСтрокаОтбрасываетДробнуюЧасть"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЗаписьJsonНаивнойДаты"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЗаписьJsonОтбрасываетДробнуюЧасть"); + ВсеТесты.Добавить("Тест_ДолженПроверить_ЗаписьJsonИгнорируетKind"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЧтениеJsonБезИменСвойствДат"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ЧтениеJsonСИменамиСвойствДат"); + + ВсеТесты.Добавить("Тест_ДолженПроверить_ПрочитатьДатуJsonОтбрасываетДробнуюЧасть"); + + Возврат ВсеТесты; + +КонецФункции + +Функция БазоваяДата() + + Возврат Дата(2026, 2, 1, 15, 30, 45); + +КонецФункции + +Функция БезДробнойЧасти(Знач Дата) + + Возврат Дата(Год(Дата), Месяц(Дата), День(Дата), Час(Дата), Минута(Дата), Секунда(Дата)); + +КонецФункции + +Функция ТекстJsonДляЧтенияДат() + + Возврат "{""a"":""2026-02-01T15:30:45"", + |""b"":""2026-02-01T15:30:45.123"", + |""c"":""2026-02-01T15:30:45Z"", + |""d"":""2026-02-01T15:30:45+03:00"", + |""e"":""2026-02-01T15:30:45.1234567Z""}"; + +КонецФункции + +Функция ОжидаемаяЛокальнаяДатаИзUtc(Год, Месяц, День, Час, Минута, Секунда) + + Возврат БезДробнойЧасти(МестноеВремя(Дата(Год, Месяц, День, Час, Минута, Секунда))); + +КонецФункции + +Процедура Тест_ДолженПроверить_ПрибавкаВышеТочности1СМеняетЗначение() Экспорт + + База = БазоваяДата(); + Дельты = Новый Массив; + Дельты.Добавить(0.001); + Дельты.Добавить(0.1); + Дельты.Добавить(0.5); + Дельты.Добавить(0.999); + Дельты.Добавить(0.0001); + + Для Каждого Дельта Из Дельты Цикл + юТест.ПроверитьЛожь(База = (База + Дельта), "Прибавка " + Дельта + " должна менять значение"); + КонецЦикла; + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ПрибавкаНижеПоловиныШага1СИгнорируется() Экспорт + + База = БазоваяДата(); + Дельты = Новый Массив; + Дельты.Добавить(0.00001); + Дельты.Добавить(0.0000001); + Дельты.Добавить(0.00004); + + Для Каждого Дельта Из Дельты Цикл + юТест.ПроверитьИстину(База = (База + Дельта), "Прибавка " + Дельта + " должна игнорироваться"); + КонецЦикла; + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ПрибавкаОкругляетсяДоШага1С() Экспорт + + База = БазоваяДата(); + + юТест.ПроверитьРавенство(0.0001, (База + 0.00005) - База); + юТест.ПроверитьРавенство(0.0001, (База + 0.00006) - База); + юТест.ПроверитьРавенство(0.0002, (База + 0.00016) - База); + юТест.ПроверитьРавенство(0.0003, (База + 0.00025) - База); + + юТест.ПроверитьИстину((База + 0.00006) = (База + 0.0001)); + юТест.ПроверитьЛожь((База + 0.00016) = (База + 0.0001)); + юТест.ПроверитьИстину((База + 0.00016) = (База + 0.0002)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ДробнаяЧастьНеОкругляетсяДоСекунды() Экспорт + + База = БазоваяДата(); + + юТест.ПроверитьЛожь((База + 0.6) = База); + юТест.ПроверитьЛожь((База + 0.6) = (База + 1)); + юТест.ПроверитьЛожь((База + 0.4) = База); + юТест.ПроверитьЛожь((База + 0.0004) = База); + юТест.ПроверитьЛожь((База + 0.0006) = База); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_РазностьДатВозвращаетДробныеСекунды() Экспорт + + База = БазоваяДата(); + + юТест.ПроверитьРавенство(0.5, (База + 0.5) - База); + юТест.ПроверитьРавенство(1.5, (База + 1.5) - База); + юТест.ПроверитьРавенство(0.25, (База + 0.25) - База); + юТест.ПроверитьРавенство(2, (База + 2) - База); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_РазностьДатНеЦелая() Экспорт + + База = БазоваяДата(); + Разность = (База + 0.5) - База; + + юТест.ПроверитьИстину(Разность <> Цел(Разность)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_СравнениеРазличаетДробнуюЧасть() Экспорт + + База = БазоваяДата(); + Дробная = База + 0.001; + + юТест.ПроверитьИстину(Дробная > База); + юТест.ПроверитьИстину(Дробная >= База); + юТест.ПроверитьЛожь(Дробная < База); + юТест.ПроверитьЛожь(База = Дробная); + юТест.ПроверитьИстину(База <> Дробная); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ТекущаяДатаБезДолейСекунды() Экспорт + + ТекДата = ТекущаяДата(); + + юТест.ПроверитьРавенство(ТекДата, БезДробнойЧасти(ТекДата)); + юТест.ПроверитьРавенство(0, ТекДата - БезДробнойЧасти(ТекДата)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ТекущаяУниверсальнаяДатаБезДолейСекунды() Экспорт + + УнивДата = ТекущаяУниверсальнаяДата(); + + юТест.ПроверитьРавенство(УнивДата, БезДробнойЧасти(УнивДата)); + юТест.ПроверитьРавенство(0, УнивДата - БезДробнойЧасти(УнивДата)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_МестноеВремяБезДолейСекунды() Экспорт + + Унив = БезДробнойЧасти(ТекущаяУниверсальнаяДата()); + Местн = МестноеВремя(Унив); + + юТест.ПроверитьРавенство(Местн, БезДробнойЧасти(Местн)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_КонвертацияПоясаСохраняетДробь() Экспорт + + Дробная = БазоваяДата() + 0.001; + + юТест.ПроверитьРавенство(Дробная, МестноеВремя(УниверсальноеВремя(Дробная))); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_СтрокаСкрываетДробнуюЧасть() Экспорт + + База = БазоваяДата(); + Дробная = База + 0.123; + + юТест.ПроверитьРавенство(Строка(База), Строка(Дробная)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ФорматИгнорируетДробнуюЧасть() Экспорт + + Дробная = БазоваяДата() + 0.123; + + юТест.ПроверитьРавенство("45", Формат(Дробная, "ДФ=сс")); + юТест.ПроверитьРавенство("20260201153045", Формат(Дробная, "ДФ=ггггММддЧЧммсс")); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_XMLСтрокаОтбрасываетДробнуюЧасть() Экспорт + + Дробная = БазоваяДата() + 0.123; + + юТест.ПроверитьРавенство("2026-02-01T15:30:45", XMLСтрока(Дробная)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЗаписьJsonНаивнойДаты() Экспорт + + Данные = Новый Структура; + Данные.Вставить("Литерал", БазоваяДата()); + Данные.Вставить("ПустаяДата", Дата(1, 1, 1)); + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + Текст = Запись.Закрыть(); + + юТест.ПроверитьВхождение(Текст, """Литерал"": ""2026-02-01T15:30:45"""); + юТест.ПроверитьВхождение(Текст, """ПустаяДата"": ""0001-01-01T00:00:00"""); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЗаписьJsonОтбрасываетДробнуюЧасть() Экспорт + + Данные = Новый Структура; + Данные.Вставить("Дробная", БазоваяДата() + 0.123); + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + Текст = Запись.Закрыть(); + + юТест.ПроверитьВхождение(Текст, """Дробная"": ""2026-02-01T15:30:45"""); + юТест.ПроверитьЛожь(СтрНайти(Текст, ".123") > 0); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЗаписьJsonИгнорируетKind() Экспорт + + Данные = Новый Структура; + Данные.Вставить("ТекущаяДата", БезДробнойЧасти(ТекущаяДата())); + Данные.Вставить("УниверсальнаяДата", БезДробнойЧасти(ТекущаяУниверсальнаяДата())); + + Запись = Новый ЗаписьJSON(); + Запись.УстановитьСтроку(); + ЗаписатьJSON(Запись, Данные); + Текст = Запись.Закрыть(); + + юТест.ПроверитьЛожь(СтрНайти(Текст, "Z") > 0); + юТест.ПроверитьЛожь(СтрНайти(Текст, "+") > 0); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЧтениеJsonБезИменСвойствДат() Экспорт + + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТекстJsonДляЧтенияДат()); + Данные = ПрочитатьJSON(Чтение); + Чтение.Закрыть(); + + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.a)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.b)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.c)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.d)); + юТест.ПроверитьРавенство(Тип("Строка"), ТипЗнч(Данные.e)); + + юТест.ПроверитьРавенство("2026-02-01T15:30:45", Данные.a); + юТест.ПроверитьРавенство("2026-02-01T15:30:45.123", Данные.b); + юТест.ПроверитьРавенство("2026-02-01T15:30:45Z", Данные.c); + юТест.ПроверитьРавенство("2026-02-01T15:30:45+03:00", Данные.d); + юТест.ПроверитьРавенство("2026-02-01T15:30:45.1234567Z", Данные.e); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ЧтениеJsonСИменамиСвойствДат() Экспорт + + ИменаСвойств = Новый Массив; + ИменаСвойств.Добавить("a"); + ИменаСвойств.Добавить("b"); + ИменаСвойств.Добавить("c"); + ИменаСвойств.Добавить("d"); + ИменаСвойств.Добавить("e"); + + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТекстJsonДляЧтенияДат()); + Данные = ПрочитатьJSON(Чтение, Ложь, ИменаСвойств, ФорматДатыJSON.ISO); + Чтение.Закрыть(); + + ОжиданиеC = ОжидаемаяЛокальнаяДатаИзUtc(2026, 2, 1, 15, 30, 45); + ОжиданиеD = ОжидаемаяЛокальнаяДатаИзUtc(2026, 2, 1, 12, 30, 45); + + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.a)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.b)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.c)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.d)); + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Данные.e)); + + юТест.ПроверитьРавенство(Дата(2026, 2, 1, 15, 30, 45), Данные.a); + юТест.ПроверитьРавенство(Дата(2026, 2, 1, 15, 30, 45), Данные.b); + юТест.ПроверитьРавенство(ОжиданиеC, Данные.c); + юТест.ПроверитьРавенство(ОжиданиеD, Данные.d); + юТест.ПроверитьРавенство(ОжиданиеC, Данные.e); + + юТест.ПроверитьИстину(Данные.a = БезДробнойЧасти(Данные.a)); + юТест.ПроверитьИстину(Данные.b = БезДробнойЧасти(Данные.b)); + юТест.ПроверитьИстину(Данные.c = БезДробнойЧасти(Данные.c)); + юТест.ПроверитьИстину(Данные.d = БезДробнойЧасти(Данные.d)); + юТест.ПроверитьИстину(Данные.e = БезДробнойЧасти(Данные.e)); + +КонецПроцедуры + +Процедура Тест_ДолженПроверить_ПрочитатьДатуJsonОтбрасываетДробнуюЧасть() Экспорт + + Результат = ПрочитатьДатуJSON("2026-02-01T15:30:45.123", ФорматДатыJSON.ISO); + + юТест.ПроверитьРавенство(Дата(2026, 2, 1, 15, 30, 45), Результат); + юТест.ПроверитьИстину(Результат = БезДробнойЧасти(Результат)); + +КонецПроцедуры diff --git a/tests/json/test-json_reader.os b/tests/json/test-json_reader.os index 3a49b33ab..61c1a8166 100644 --- a/tests/json/test-json_reader.os +++ b/tests/json/test-json_reader.os @@ -33,7 +33,8 @@ СписокТестов.Добавить("Тест_Должен_ЧитатьСтрокуСДатойВФорматеISOКакСтроку"); СписокТестов.Добавить("Тест_Должен_ОткрытьПотокЧитаетСтрокуСДатойВФорматеISOКакСтроку"); СписокТестов.Добавить("Тест_Должен_ОткрытьФайлЧитаетСтрокуСДатойВФорматеISOКакСтроку"); - СписокТестов.Добавить("Тест_Должен_ЧитатьДатуВФорматеISOКакСтрокуПриИспользованииПрочитатьJSON"); + СписокТестов.Добавить("Тест_Должен_ЧитатьДатуВФорматеISOКакСтрокуПриИспользованииПрочитатьJSON"); + СписокТестов.Добавить("Тест_Должен_ЧитатьДатуВФорматеISOПриИспользованииПрочитатьJSONСИменамиСвойствДат"); Возврат СписокТестов; @@ -333,6 +334,26 @@ КонецПроцедуры +Процедура Тест_Должен_ЧитатьДатуВФорматеISOПриИспользованииПрочитатьJSONСИменамиСвойствДат() Экспорт + + СтрокаJSON = "{ ""lastSignInTime"": ""2026-05-13T22:51:46.230Z"" }"; + + ИменаСвойств = Новый Массив; + ИменаСвойств.Добавить("lastSignInTime"); + + ЧтениеJSON = Новый ЧтениеJSON; + ЧтениеJSON.УстановитьСтроку(СтрокаJSON); + Объект = ПрочитатьJSON(ЧтениеJSON, Ложь, ИменаСвойств, ФорматДатыJSON.ISO); + ЧтениеJSON.Закрыть(); + + Ожидание = МестноеВремя(Дата(2026, 5, 13, 22, 51, 46)); + + юТест.ПроверитьРавенство(Тип("Дата"), ТипЗнч(Объект.lastSignInTime)); + юТест.ПроверитьРавенство(Ожидание, Объект.lastSignInTime); + юТест.ПроверитьРавенство(0, Объект.lastSignInTime - Дата(Год(Объект.lastSignInTime), Месяц(Объект.lastSignInTime), День(Объект.lastSignInTime), Час(Объект.lastSignInTime), Минута(Объект.lastSignInTime), Секунда(Объект.lastSignInTime))); + +КонецПроцедуры + Процедура Тест_Должен_ОткрытьПотокЧитаетСтрокуСДатойВФорматеISOКакСтроку() Экспорт