diff --git a/Px.Utils.UnitTests/ModelBuilderTests/Fixtures/PxFileMetaEntries_Robust_1_Language_With_Table_Level_Units.cs b/Px.Utils.UnitTests/ModelBuilderTests/Fixtures/PxFileMetaEntries_Robust_1_Language_With_Table_Level_Units.cs new file mode 100644 index 00000000..0539ff37 --- /dev/null +++ b/Px.Utils.UnitTests/ModelBuilderTests/Fixtures/PxFileMetaEntries_Robust_1_Language_With_Table_Level_Units.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Px.Utils.UnitTests.ModelBuilderTests.Fixtures +{ + internal static class PxFileMetaEntries_Robust_1_Language_With_Table_Level_Units_And_Precision + { + public static List> Entries = + [ + new("CHARSET", "\"ANSI\""), + new("AXIS-VERSION", "\"2013\""), + new("CODEPAGE", "\"iso-8859-15\""), + new("LANGUAGE", "\"fi\""), + new("CREATION-DATE", "\"20200121 09:00\""), + new("NEXT-UPDATE", "\"20240131 08:00\""), + new("TABLEID", "\"example_table_id_for_testing\""), + new("DECIMALS", "0"), + new("SHOWDECIMALS", "1"), + new("MATRIX", "\"001_12ab_2022\""), + new("SUBJECT-CODE", "\"ABCD\""), + new("SUBJECT-AREA", "\"abcd\""), + new("COPYRIGHT", "YES"), + new("DESCRIPTION", "\"test_description_fi\""), + new("TITLE", "\"test_title_fi\""), + new("CONTENTS", "\"test_contents_fi\""), + new("UNITS", "\"test_unit_fi\""), + new("STUB", "\"Vuosi\",\"Alue\",\"Talotyyppi\""), + new("HEADING", "\"Tiedot\""), + new("CONTVARIABLE", "\"Tiedot\""), + new("VALUES(\"Vuosi\")", "\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\""), + new("VALUES(\"Alue\")", "\"Koko maa\",\"Pääkaupunkiseutu (PKS)\",\"Muu Suomi (koko maa pl. PKS)\",\"Helsinki\", \"Espoo-Kauniainen\",\"Vantaa\",\"Turku\""), + new("VALUES(\"Talotyyppi\")", "\"Talotyypit yhteensä\",\"Rivitalot\",\"Kerrostalot\""), + new("VALUES(\"Tiedot\")", "\"Indeksi (2015=100)\",\"Muutos edelliseen vuoteen (indeksi 2015=100)\",\"Kauppojen lukumäärä\""), + new("TIMEVAL(\"Vuosi\")", "TLIST(A1),\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\""), + new("CODES(\"Vuosi\")", "\"2015\",\"2016\",\"2017\",\"2018\",\"2019\",\"2020\",\"2021\",\"2022\""), + new("CODES(\"Alue\")", "\"ksu\",\"pks\",\"msu\",\"091\",\"049\",\"092\",\"853\""), + new("CODES(\"Talotyyppi\")", "\"0\",\"1\",\"3\""), + new("CODES(\"Tiedot\")", "\"ketjutettu_lv\",\"vmuutos_lv\",\"lkm_julk_uudet\""), + new("VARIABLE-TYPE(\"Vuosi\")", "\"Time\""), + new("VARIABLE-TYPE(\"Alue\")", "\"Classificatory\""), + new("VARIABLE-TYPE(\"Talotyyppi\")", "\"Classificatory\""), + new("MAP(\"Alue\")", "\"Alue 2018\""), + new("ELIMINATION(\"Talotyyppi\")", "\"Talotyypit yhteensä\""), + new("PRECISION(\"Tiedot\",\"Muutos edelliseen vuoteen (indeksi 2015=100)\")", "1"), + new("LAST-UPDATED(\"Indeksi (2015=100)\")", "\"20230131 08:00\""), + new("LAST-UPDATED(\"Muutos edelliseen vuoteen (indeksi 2015=100)\")", "\"20230131 09:00\""), + new("LAST-UPDATED(\"Kauppojen lukumäärä\")", "\"20230131 10:00\""), + new("UNITS(\"Indeksi (2015=100)\")", "\"indeksipisteluku\""), + new("UNITS(\"Muutos edelliseen vuoteen (indeksi 2015=100)\")", "\"%\""), + new("UNITS", "\"lukumäärä\""), // table level units + new("CONTACT(\"Indeksi (2015=100)\")", "\"test_contact1_fi\""), + new("CONTACT(\"Muutos edelliseen vuoteen (indeksi 2015=100)\")", "\"test_contact2_fi\""), + new("CONTACT(\"Kauppojen lukumäärä\")", "\"test_contact3_fi\""), + new("SOURCE", "\"test_source_fi\""), + new("OFFICIAL-STATISTICS", "YES"), + new("NOTE", "\"test_note_fi\""), + new("NOTE(\"Talotyyppi\")", "\"test_note_talotyyppi\""), + new("VALUENOTE(\"Tiedot\",\"Indeksi (2015=100)\")", "\"test_value_note_tiedot_indeksi\""), + new("VALUENOTE(\"Tiedot\",\"Muutos edelliseen vuoteen (indeksi 2015=100)\")", "\"test_value_note_tiedot_muutos\""), + new("VALUENOTE(\"Tiedot\",\"Kauppojen lukumäärä\")", "\"test_value_note_tiedot_kauppojen_lukumäärä\"") + ]; + } +} diff --git a/Px.Utils.UnitTests/ModelBuilderTests/MatrixMetadataBuilderTests.cs b/Px.Utils.UnitTests/ModelBuilderTests/MatrixMetadataBuilderTests.cs index e8717e21..345fbf24 100644 --- a/Px.Utils.UnitTests/ModelBuilderTests/MatrixMetadataBuilderTests.cs +++ b/Px.Utils.UnitTests/ModelBuilderTests/MatrixMetadataBuilderTests.cs @@ -5,6 +5,8 @@ using Px.Utils.Models.Metadata.Dimensions; using Px.Utils.Models.Metadata.Enums; using Px.Utils.Models.Metadata.MetaProperties; +using Px.Utils.PxFile; +using Px.Utils.UnitTests.ModelBuilderTests.Fixtures; using System.Globalization; using Px.Utils.PxFile; using Px.Utils.UnitTests.ModelBuilderTests.Fixtures; @@ -17,6 +19,8 @@ public class MatrixMetadataBuilderTests private MatrixMetadata Actual_3Lang { get; } = new MatrixMetadataBuilder().Build(PxFileMetaEntries_Robust_3_Languages.Entries); private MatrixMetadata Actual_1Lang { get; } = new MatrixMetadataBuilder().Build(PxFileMetaEntries_Robust_1_Language.Entries); private MatrixMetadata Actual_Recommended_3Lang { get; } = new MatrixMetadataBuilder().Build(PxFileMetaEntries_Recommended_3_Langs.Entries); + private MatrixMetadata Actual_1Lang_With_Table_Level_Units_And_Precision { get; } = + new MatrixMetadataBuilder().Build(PxFileMetaEntries_Robust_1_Language_With_Table_Level_Units_And_Precision.Entries); [TestMethod] public void IEnumerableBuildTest() @@ -206,6 +210,29 @@ public void SingleLangVariableBuildTest() CollectionAssert.AreEqual(expectedNames, Actual_1Lang.Dimensions.Select(d => d.Name).ToList()); } + [TestMethod] + public void SingleLangWithTableLevelUnitsAndPrecisionBuildTest() + { + ContentDimension? contentDimension = (ContentDimension?)Actual_1Lang_With_Table_Level_Units_And_Precision.Dimensions.Find(d => d.Type == DimensionType.Content); + Assert.IsNotNull(contentDimension); + MultilanguageString[] expectedUnits = [ + new("fi", "indeksipisteluku"), + new("fi", "%"), + new("fi", "lukumäärä") + ]; + for(int i = 0; i < contentDimension.Values.Count; i++) + { + Assert.AreEqual(expectedUnits[i], contentDimension.Values[i].Unit); + } + for(int i = 0; i < contentDimension.Values.Count; i++) + { + Assert.AreEqual(1, contentDimension.Values[i].Precision); + } + Assert.IsFalse(Actual_1Lang_With_Table_Level_Units_And_Precision.AdditionalProperties.ContainsKey(PxFileSyntaxConf.Default.Tokens.KeyWords.Units)); + Assert.IsFalse(Actual_1Lang_With_Table_Level_Units_And_Precision.AdditionalProperties.ContainsKey(PxFileSyntaxConf.Default.Tokens.KeyWords.Decimals)); + Assert.IsFalse(Actual_1Lang_With_Table_Level_Units_And_Precision.AdditionalProperties.ContainsKey(PxFileSyntaxConf.Default.Tokens.KeyWords.ShowDecimals)); + } + #region Content Dimension Tests [TestMethod] diff --git a/Px.Utils.UnitTests/SerializerTests/Fixtures/Json/MatrixMetadataJson.cs b/Px.Utils.UnitTests/SerializerTests/Fixtures/Json/MatrixMetadataJson.cs index 143ba658..4a0af371 100644 --- a/Px.Utils.UnitTests/SerializerTests/Fixtures/Json/MatrixMetadataJson.cs +++ b/Px.Utils.UnitTests/SerializerTests/Fixtures/Json/MatrixMetadataJson.cs @@ -95,5 +95,96 @@ internal static class MatrixMetadataJson public static string SimpleMetaWithoutWhitespace = @"{""DefaultLanguage"":""foo"",""AvailableLanguages"":[""foo"",""bar""],""Dimensions"":[{""Type"":""Nominal"",""Code"":""dimension_code_1"",""Name"":{""foo"":""dimension_name_1_foo"",""bar"":""dimension_name_1_bar""},""Values"":[{""Code"":""value_code_1_1"",""Name"":{""foo"":""value_name_1_1_foo"",""bar"":""value_name_1_1_bar""},""AdditionalProperties"":{},""IsVirtual"":false}],""AdditionalProperties"":{}},{""Type"":""Content"",""Code"":""dimension_code_2"",""Name"":{""foo"":""dimension_name_2_foo"",""bar"":""dimension_name_2_bar""},""Values"":[{""Unit"":{""foo"":""unit_name_2_1_foo"",""bar"":""unit_name_2_1_bar""},""LastUpdated"":""0001-01-01T00:00:00"",""Precision"":1,""Code"":""value_code_2_1"",""Name"":{""foo"":""value_name_2_1_foo"",""bar"":""value_name_2_1_bar""},""AdditionalProperties"":{},""IsVirtual"":false}],""AdditionalProperties"":{}},{""Type"":""Time"",""Code"":""dimension_code_3"",""Name"":{""foo"":""dimension_name_3_foo"",""bar"":""dimension_name_3_bar""},""Interval"":""Year"",""Values"":[{""Code"":""value_code_3_1"",""Name"":{""foo"":""value_name_3_1_foo"",""bar"":""value_name_3_1_bar""},""AdditionalProperties"":{},""IsVirtual"":false}],""AdditionalProperties"":{}}],""AdditionalProperties"":{""property_1"":{""Type"":""Text"",""Value"":""property_value_1""},""property_2"":{""Type"":""MultilanguageText"",""Value"":{""foo"":""property_value_foo"",""bar"":""property_value_bar""}}}}"; + + public static string SimpleMetaPascalCase = + @"{ + ""DefaultLanguage"": ""foo"", + ""AvailableLanguages"": [ + ""foo"", + ""bar"" + ], + ""Dimensions"": [ + { + ""Type"": ""Nominal"", + ""Code"": ""dimension_code_1"", + ""Name"": { + ""foo"": ""dimension_name_1_foo"", + ""bar"": ""dimension_name_1_bar"" + }, + ""Values"": [ + { + ""Code"": ""value_code_1_1"", + ""Name"": { + ""foo"": ""value_name_1_1_foo"", + ""bar"": ""value_name_1_1_bar"" + }, + ""AdditionalProperties"": {}, + ""IsVirtual"": false + } + ], + ""AdditionalProperties"": {} + }, + { + ""Type"": ""Content"", + ""Code"": ""dimension_code_2"", + ""Name"": { + ""foo"": ""dimension_name_2_foo"", + ""bar"": ""dimension_name_2_bar"" + }, + ""Values"": [ + { + ""Unit"": { + ""foo"": ""unit_name_2_1_foo"", + ""bar"": ""unit_name_2_1_bar"" + }, + ""LastUpdated"": ""0001-01-01T00:00:00"", + ""Precision"": 1, + ""Code"": ""value_code_2_1"", + ""Name"": { + ""foo"": ""value_name_2_1_foo"", + ""bar"": ""value_name_2_1_bar"" + }, + ""AdditionalProperties"": {}, + ""IsVirtual"": false + } + ], + ""AdditionalProperties"": {} + }, + { + ""Type"": ""Time"", + ""Code"": ""dimension_code_3"", + ""Name"": { + ""foo"": ""dimension_name_3_foo"", + ""bar"": ""dimension_name_3_bar"" + }, + ""Interval"": ""Year"", + ""Values"": [ + { + ""Code"": ""value_code_3_1"", + ""Name"": { + ""foo"": ""value_name_3_1_foo"", + ""bar"": ""value_name_3_1_bar"" + }, + ""AdditionalProperties"": {}, + ""IsVirtual"": false + } + ], + ""AdditionalProperties"": {} + } + ], + ""additionalProperties"": { + ""property_1"": { + ""type"": ""Text"", + ""value"": ""property_value_1"" + }, + ""property_2"": { + ""type"": ""MultilanguageText"", + ""value"": { + ""foo"": ""property_value_foo"", + ""bar"": ""property_value_bar"" + } + } + } +}"; } } diff --git a/Px.Utils.UnitTests/SerializerTests/MatrixMetadataConverterSerializeTests.cs b/Px.Utils.UnitTests/SerializerTests/MatrixMetadataConverterSerializeTests.cs index b3615f6a..182f85b4 100644 --- a/Px.Utils.UnitTests/SerializerTests/MatrixMetadataConverterSerializeTests.cs +++ b/Px.Utils.UnitTests/SerializerTests/MatrixMetadataConverterSerializeTests.cs @@ -11,7 +11,8 @@ public class MatrixMetadataConverterSerializeTests private static readonly JsonSerializerOptions IndentedCachedOptions = new() { WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true }; private static readonly JsonSerializerOptions OneLineCachedOptions = new() @@ -57,5 +58,14 @@ public void SerializeMetadataToOneLineMatchesFixture() string output = JsonSerializer.Serialize(MatrixMetadataFixture.SimpleMeta, OneLineCachedOptions); Assert.AreEqual(MatrixMetadataJson.SimpleMetaWithoutWhitespace, output); } + + [TestMethod] + public void DeserializeMetadataFromPascalCaseJsonReturnsCorrectObject() + { + MatrixMetadata? deserialized = JsonSerializer.Deserialize(MatrixMetadataJson.SimpleMetaPascalCase, IndentedCachedOptions); + string output = JsonSerializer.Serialize(deserialized, IndentedCachedOptions); + string expected = JsonSerializer.Serialize(MatrixMetadataFixture.SimpleMeta, IndentedCachedOptions); + Assert.AreEqual(expected, output); + } } } diff --git a/Px.Utils.UnitTests/SerializerTests/MultilanguageStringConverterTests.cs b/Px.Utils.UnitTests/SerializerTests/MultilanguageStringConverterTests.cs index 9422c07f..bb1f4341 100644 --- a/Px.Utils.UnitTests/SerializerTests/MultilanguageStringConverterTests.cs +++ b/Px.Utils.UnitTests/SerializerTests/MultilanguageStringConverterTests.cs @@ -107,5 +107,18 @@ public void DeserializeMultilanguageStringWithNoLanguages() Assert.IsNotNull(deserialized); Assert.AreEqual(0, deserialized.Languages.Count()); } + + [TestMethod] + public void DeserializeMultilanguageStringWithNull() + { + // Arrange + string serialized = "null"; + + // Act + MultilanguageString? deserialized = JsonSerializer.Deserialize(serialized); + + // Assert + Assert.IsNull(deserialized); + } } } diff --git a/Px.Utils/ModelBuilders/MatrixMetadataBuilder.cs b/Px.Utils/ModelBuilders/MatrixMetadataBuilder.cs index d2f2e135..1fd8645c 100644 --- a/Px.Utils/ModelBuilders/MatrixMetadataBuilder.cs +++ b/Px.Utils/ModelBuilders/MatrixMetadataBuilder.cs @@ -226,7 +226,19 @@ private ContentDimension BuildContentDimension(Dictionary? unitEntries)) + { + foreach (MetadataEntryKey key in unitEntries.Keys) entries.Remove(key); + } + if (TryGetEntries(entries, _pxFileSyntaxConf.Tokens.KeyWords.Decimals, langs, out Dictionary? decimalEntries)) + { + foreach (MetadataEntryKey key in decimalEntries.Keys) entries.Remove(key); + } + if (TryGetEntries(entries, _pxFileSyntaxConf.Tokens.KeyWords.ShowDecimals, langs, out Dictionary? showDecimalEntries)) + { + foreach (MetadataEntryKey key in showDecimalEntries.Keys) entries.Remove(key); + } return new ContentDimension(code, dimensionName, [], values); } @@ -391,19 +403,32 @@ private string GetDimensionCode(Dictionary entries, Px private MultilanguageString GetUnit(Dictionary entries, PxFileLanguages langs, MultilanguageString dimName, MultilanguageString valName) { string unitKey = _pxFileSyntaxConf.Tokens.KeyWords.Units; + // If table level unit is used, the unit key is not associated with a specific dimension value and is removed after building the content dimension + bool tableLevelUnitUsed; if (TryGetEntries(entries, unitKey, langs, out Dictionary? unitEntries, dimName, valName) || // Both identifiers - TryGetEntries(entries, unitKey, langs, out unitEntries, valName) || // Only value identifier - TryGetEntries(entries, unitKey, langs, out unitEntries)) // No identifiers + TryGetEntries(entries, unitKey, langs, out unitEntries, valName)) // One identifier + { + tableLevelUnitUsed = false; + } + else if (TryGetEntries(entries, unitKey, langs, out unitEntries)) // No identifiers + { + tableLevelUnitUsed = true; + } + else { - Dictionary translations = []; - foreach (KeyValuePair kvp in unitEntries) + throw new ArgumentException("Unit information not found"); + } + + Dictionary translations = []; + foreach (KeyValuePair kvp in unitEntries) + { + translations[kvp.Key.Language ?? langs.DefaultLanguage] = kvp.Value.CleanStringDelimeters(_stringDelimeter); + if (!tableLevelUnitUsed) { - translations[kvp.Key.Language ?? langs.DefaultLanguage] = kvp.Value.CleanStringDelimeters(_stringDelimeter); entries.Remove(kvp.Key); } - return new MultilanguageString(translations); } - throw new ArgumentException("Unit information not found"); + return new MultilanguageString(translations); } private DateTime GetLastUpdated(Dictionary entries, PxFileLanguages langs, MultilanguageString dimName, MultilanguageString valName) @@ -425,14 +450,28 @@ private DateTime GetLastUpdated(Dictionary entries, Px private int GetPrecision(Dictionary entries, PxFileLanguages langs, MultilanguageString dimName, MultilanguageString valName) { string precisionKey = _pxFileSyntaxConf.Tokens.KeyWords.Precision; - if ((TryGetEntries(entries, precisionKey, langs, out Dictionary? precisionEntries, dimName, valName) || // Both identifiers + // If table level precision is used, the precision key is not associated with a specific dimension value and is removed after building the content dimension + bool tableLevelPrecisionUsed = false; + if (TryGetEntries(entries, precisionKey, langs, out Dictionary? precisionEntries, dimName, valName) || // Both identifiers TryGetEntries(entries, precisionKey, langs, out precisionEntries, valName)) // Only value identifier - && int.TryParse(precisionEntries.Values.First(), out int result)) { - foreach (MetadataEntryKey key in precisionEntries.Keys) entries.Remove(key); - return result; + tableLevelPrecisionUsed = false; + } + // No identifiers, table level precision using SHOWDECIMALS and DECIMALS keyword + else if (TryGetEntries(entries, _pxFileSyntaxConf.Tokens.KeyWords.ShowDecimals, langs, out precisionEntries) || + TryGetEntries(entries, _pxFileSyntaxConf.Tokens.KeyWords.Decimals, langs, out precisionEntries)) + { + tableLevelPrecisionUsed = true; } + if (precisionEntries is not null && int.TryParse(precisionEntries.Values.First(), out int result)) + { + if (!tableLevelPrecisionUsed) + { + foreach (MetadataEntryKey key in precisionEntries.Keys) entries.Remove(key); + } + return result; + } return 0; // Default value } diff --git a/Px.Utils/Models/Metadata/ExtensionMethods/MatrixMetadataExtensions.cs b/Px.Utils/Models/Metadata/ExtensionMethods/MatrixMetadataExtensions.cs index aed8cec2..19fcd7da 100644 --- a/Px.Utils/Models/Metadata/ExtensionMethods/MatrixMetadataExtensions.cs +++ b/Px.Utils/Models/Metadata/ExtensionMethods/MatrixMetadataExtensions.cs @@ -24,10 +24,10 @@ public static ContentDimension GetContentDimension(this IReadOnlyMatrixMetadata /// /// object representing the first dimension with type . /// True if the content dimension is found in the metadata, false otherwise. - public static bool TryGetContentDimension(this IReadOnlyMatrixMetadata metadata, [MaybeNullWhen(false)] out ContentDimension? contentDimension) + public static bool TryGetContentDimension(this IReadOnlyMatrixMetadata metadata, [MaybeNullWhen(false)] out ContentDimension contentDimension) { contentDimension = metadata.Dimensions.FirstOrDefault(dimension => dimension.Type == Enums.DimensionType.Content) as ContentDimension; - return contentDimension != null; + return contentDimension is not null; } /// @@ -46,10 +46,10 @@ public static TimeDimension GetTimeDimension(this IReadOnlyMatrixMetadata metada /// /// object representing the first dimension with type . /// True if the time dimension is found in the metadata, false otherwise. - public static bool TryGetTimeDimension(this IReadOnlyMatrixMetadata metadata, [MaybeNullWhen(false)] out TimeDimension? timeDimension) + public static bool TryGetTimeDimension(this IReadOnlyMatrixMetadata metadata, [MaybeNullWhen(false)] out TimeDimension timeDimension) { timeDimension = metadata.Dimensions.FirstOrDefault(dimension => dimension.Type == Enums.DimensionType.Time) as TimeDimension; - return timeDimension != null; + return timeDimension is not null; } } } diff --git a/Px.Utils/PxFile/Data/IPxFileStreamDataReader.cs b/Px.Utils/PxFile/Data/IPxFileStreamDataReader.cs index ed29b9de..2e1d7dd9 100644 --- a/Px.Utils/PxFile/Data/IPxFileStreamDataReader.cs +++ b/Px.Utils/PxFile/Data/IPxFileStreamDataReader.cs @@ -1,5 +1,4 @@ -using Px.Utils.PxFile.Data; -using Px.Utils.Models.Data.DataValue; +using Px.Utils.Models.Data.DataValue; namespace Px.Utils.PxFile.Data { diff --git a/Px.Utils/PxFile/Data/PxFileStreamDataReader.cs b/Px.Utils/PxFile/Data/PxFileStreamDataReader.cs index fa88e080..e7f5cf18 100644 --- a/Px.Utils/PxFile/Data/PxFileStreamDataReader.cs +++ b/Px.Utils/PxFile/Data/PxFileStreamDataReader.cs @@ -1,5 +1,4 @@ -using Px.Utils.PxFile.Data; -using Px.Utils.Models.Data.DataValue; +using Px.Utils.Models.Data.DataValue; namespace Px.Utils.PxFile.Data { @@ -10,6 +9,7 @@ namespace Px.Utils.PxFile.Data public sealed class PxFileStreamDataReader : IPxFileStreamDataReader, IDisposable { private readonly Stream _stream; + private bool _disposed; // default false private readonly PxFileSyntaxConf _conf; private long readIndex; private readonly int _readBufferSize; @@ -242,7 +242,11 @@ await Task.Factory.StartNew(() => /// public void Dispose() { - _stream.Dispose(); + if(!_disposed) + { + _stream.Dispose(); + _disposed = true; + } } #region Private methods diff --git a/Px.Utils/PxFile/PxFileSyntaxConf.cs b/Px.Utils/PxFile/PxFileSyntaxConf.cs index adebf932..c1c09e7a 100644 --- a/Px.Utils/PxFile/PxFileSyntaxConf.cs +++ b/Px.Utils/PxFile/PxFileSyntaxConf.cs @@ -145,6 +145,8 @@ public class KeyWordTokens private const string UNITS = "UNITS"; private const string LAST_UPDATED = "LAST-UPDATED"; private const string PRECISION = "PRECISION"; + private const string DECIMALS = "DECIMALS"; + private const string SHOWDECIMALS = "SHOWDECIMALS"; private const string DIMENSION_DEFAULT_VALUE = "ELIMINATION"; private const string TIME_VAL = "TIMEVAL"; private const string MAP = "MAP"; @@ -168,6 +170,8 @@ public class KeyWordTokens public string Units { get; set; } = UNITS; public string LastUpdated { get; set; } = LAST_UPDATED; public string Precision { get; set; } = PRECISION; + public string Decimals { get; set; } = DECIMALS; + public string ShowDecimals { get; set; } = SHOWDECIMALS; public string DimensionDefaultValue { get; set; } = DIMENSION_DEFAULT_VALUE; public string TimeVal { get; set; } = TIME_VAL; public string Map { get; set; } = MAP; diff --git a/Px.Utils/Serializers/Json/DimensionConverter.cs b/Px.Utils/Serializers/Json/DimensionConverter.cs index 6e3497c7..15fb217c 100644 --- a/Px.Utils/Serializers/Json/DimensionConverter.cs +++ b/Px.Utils/Serializers/Json/DimensionConverter.cs @@ -29,37 +29,37 @@ public override bool CanConvert(Type typeToConvert) JsonElement root = doc.RootElement; string typeName = FN(nameof(IReadOnlyDimension.Type), options); - DimensionType type = root.GetProperty(typeName).Deserialize(options); + DimensionType type = root.GetProperty(typeName, options).Deserialize(options); string codeName = FN(nameof(IReadOnlyDimension.Code), options); - string code = root.GetProperty(codeName).GetString() + string code = root.GetProperty(codeName, options).GetString() ?? throw new JsonException("Code property not found."); string nameName = FN(nameof(IReadOnlyDimension.Name), options); - MultilanguageString name = root.GetProperty(nameName).Deserialize(options) + MultilanguageString name = root.GetProperty(nameName, options).Deserialize(options) ?? throw new JsonException("Name property not found."); string additionalPropertiesName = FN(nameof(IReadOnlyDimension.AdditionalProperties), options); - Dictionary additionalProperties = root.GetProperty(additionalPropertiesName) + Dictionary additionalProperties = root.GetProperty(additionalPropertiesName, options) .Deserialize>(options) ?? throw new JsonException("AdditionalProperties property not found."); string valuesName = FN(nameof(IReadOnlyDimension.Values), options); if (type == DimensionType.Content) { - ContentValueList contentValues = root.GetProperty(valuesName).Deserialize(options) + ContentValueList contentValues = root.GetProperty(valuesName, options).Deserialize(options) ?? throw new JsonException("Values property not found."); return new ContentDimension(code, name, additionalProperties, contentValues); } - ValueList values = root.GetProperty(valuesName).Deserialize(options) + ValueList values = root.GetProperty(valuesName, options).Deserialize(options) ?? throw new JsonException("Values property not found."); if (type == DimensionType.Time) { string intervalName = FN(nameof(TimeDimension.Interval), options); - TimeDimensionInterval interval = root.GetProperty(intervalName).Deserialize(options); + TimeDimensionInterval interval = root.GetProperty(intervalName, options).Deserialize(options); return new TimeDimension(code, name, additionalProperties, values, interval); } @@ -114,6 +114,7 @@ public override void Write(Utf8JsonWriter writer, Dimension value, JsonSerialize writer.WriteEndObject(); } + private static string FN(string input, JsonSerializerOptions options) => options.PropertyNamingPolicy?.ConvertName(input) ?? input; } diff --git a/Px.Utils/Serializers/Json/ExtensionMethods.cs b/Px.Utils/Serializers/Json/ExtensionMethods.cs new file mode 100644 index 00000000..5f8c5ad1 --- /dev/null +++ b/Px.Utils/Serializers/Json/ExtensionMethods.cs @@ -0,0 +1,24 @@ +using System.Text.Json; + +namespace Px.Utils.Serializers.Json +{ + internal static class ExtensionMethods + { + /// + /// Extension method to enable case insensitive property name lookup. + /// + internal static JsonElement GetProperty(this JsonElement root, string propertyName, JsonSerializerOptions options) + { + if (options.PropertyNameCaseInsensitive) + { + return root.EnumerateObject() + .FirstOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + .Value; + } + else + { + return root.GetProperty(propertyName); + } + } + } +} diff --git a/Px.Utils/Serializers/Json/MetaPropertyConverter.cs b/Px.Utils/Serializers/Json/MetaPropertyConverter.cs index 5d607fa7..f3fda7e4 100644 --- a/Px.Utils/Serializers/Json/MetaPropertyConverter.cs +++ b/Px.Utils/Serializers/Json/MetaPropertyConverter.cs @@ -30,33 +30,33 @@ public override bool CanConvert(Type typeToConvert) JsonElement root = doc.RootElement; string typeName = options.PropertyNamingPolicy?.ConvertName(nameof(MetaProperty.Type)) ?? nameof(MetaProperty.Type); - MetaPropertyType type = root.GetProperty(typeName).Deserialize(options); + MetaPropertyType type = root.GetProperty(typeName, options).Deserialize(options); string valuePropertyName = options.PropertyNamingPolicy?.ConvertName(VALUE_PROPERTY_NAME) ?? VALUE_PROPERTY_NAME; switch (type) { case MetaPropertyType.Text: - string stringValue = root.GetProperty(valuePropertyName).GetString() + string stringValue = root.GetProperty(valuePropertyName, options).GetString() ?? throw new JsonException("Error reading string property, property named value not found."); return new StringProperty(stringValue); case MetaPropertyType.MultilanguageText: - MultilanguageString multilanguageString = root.GetProperty(valuePropertyName) + MultilanguageString multilanguageString = root.GetProperty(valuePropertyName, options) .Deserialize(options) ?? throw new JsonException("Error reading multilanguage string property, property named value not found."); return new MultilanguageStringProperty(multilanguageString); case MetaPropertyType.Numeric: - double numericValue = root.GetProperty(valuePropertyName).GetDouble(); + double numericValue = root.GetProperty(valuePropertyName, options).GetDouble(); return new NumericProperty(numericValue); case MetaPropertyType.Boolean: - bool booleanValue = root.GetProperty(valuePropertyName).GetBoolean(); + bool booleanValue = root.GetProperty(valuePropertyName, options).GetBoolean(); return new BooleanProperty(booleanValue); case MetaPropertyType.TextArray: - List strings = root.GetProperty(valuePropertyName) + List strings = root.GetProperty(valuePropertyName, options) .Deserialize>(options) ?? throw new JsonException("Error reading string list property, property named value not found."); return new StringListProperty(strings); case MetaPropertyType.MultilanguageTextArray: - List multilanguageStrings = root.GetProperty(valuePropertyName) + List multilanguageStrings = root.GetProperty(valuePropertyName, options) .Deserialize>(options) ?? throw new JsonException("Error reading multilanguage string list property, property named value not found."); return new MultilanguageStringListProperty(multilanguageStrings); diff --git a/Px.Utils/Serializers/Json/MultilanguageStringConverter.cs b/Px.Utils/Serializers/Json/MultilanguageStringConverter.cs index a5bc98dc..0e7cca9f 100644 --- a/Px.Utils/Serializers/Json/MultilanguageStringConverter.cs +++ b/Px.Utils/Serializers/Json/MultilanguageStringConverter.cs @@ -11,7 +11,7 @@ public class MultilanguageStringConverter : JsonConverter { public override MultilanguageString? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - Dictionary? translations = JsonSerializer.Deserialize>(ref reader, options); + Dictionary? translations = JsonSerializer.Deserialize?>(ref reader, options); return translations is not null ? new MultilanguageString(translations) : null; } diff --git a/Px.Utils/Validation/ContentValidation/ContentValidator.cs b/Px.Utils/Validation/ContentValidation/ContentValidator.cs index d7ecab7d..c0961e64 100644 --- a/Px.Utils/Validation/ContentValidation/ContentValidator.cs +++ b/Px.Utils/Validation/ContentValidation/ContentValidator.cs @@ -1,5 +1,4 @@ using Px.Utils.PxFile; -using Px.Utils.Validation.DatabaseValidation; using Px.Utils.Validation.SyntaxValidation; using System.Text; diff --git a/Px.Utils/Validation/DataValidation/DataValidator.cs b/Px.Utils/Validation/DataValidation/DataValidator.cs index 542b75d1..8c2598dc 100644 --- a/Px.Utils/Validation/DataValidation/DataValidator.cs +++ b/Px.Utils/Validation/DataValidation/DataValidator.cs @@ -1,6 +1,5 @@ using System.Text; using Px.Utils.PxFile; -using Px.Utils.PxFile.Metadata; using Px.Utils.Validation.DatabaseValidation; namespace Px.Utils.Validation.DataValidation diff --git a/docs/README.md b/docs/README.md index 0c4eb76d..eff21db9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,7 @@ Px.Utils is a .NET library for reading and processing px files and processing data in px-type cube format. The goal of this library is to offer a high performance and easy to use library that can be integrated into any .NET project. This project aims to follow these core design principles: +## Design goals ### High performance Everything is designed with performance in mind. Px files are commonly used in webserver environments where performance and low memory usage is important. ### Be as unopinionated as possible @@ -22,7 +23,6 @@ dotnet add package Px.Utils dotnet add package Px.Utils --version 1.0.0 ``` - ### NuGet Package Manager #### Latest ```bash @@ -36,33 +36,128 @@ nuget install Px.Utils -Version 1.0.0 ## Features ### Reading the px files -TBA + +The read pipeline consists of the following components: Reading the metadata, building the metadata and reading the data. +Each of these components can be used separately and replaced with custom implementations. +Especially if the contents of your px-files do not follow the standard px-file format, you might need to implement your own metadata builder. + +#### PxFileMetadataReader : IPxFileMetadataReader +```ReadMetadata(Stream stream, Encoding encoding)``` reads the metadata entries from the provided stream as a IEnumerable of ```KeyValuePair``` representing the keys and values of the entries. +**This method does not perform any validation on the metadata entries.** + +```ReadMetadataAsync(Stream stream, Encoding encoding)``` is an asynchronous version of the ```ReadMetadata``` method. + +#### MatrixMetadataBuilder : IMatrixMetadataBuilder +``` Build(IEnumerable> metadataInput)``` creates a ```MatrixMetadata``` object from the provided metadata entries. +The entries need to be in the same key-value format as the output of the ```PxFileMetadataReader``` ```ReadMetadata``` method. + +#### PxFileStreamDataReader : IPxFileStreamDataReader, IDisposable + ```ReadDecimalDataValues(DecimalDataValue[] buffer, int offset, DataIndexer indexer)``` reads data from the px-file into the provided buffer. There are simillary named methods for reading data in to other types of buffers. + The data will be written in to the buffer in order, starting form the offset. + +The ```DataIndexer``` generates the indexes where the data will be read from. It can be built with the map of the complete file meta and other map (targetMap) which describes the values that will be read. + +**IMPORTANT!** The target map must have the same order as the complete file map. This is for performance reasons, we do not want to move back and forth in the file or generate a second indexer for placing the data in the buffer. + +Simple example for using the datareader: +```csharp + IReadOnlyMatrixMetadata metaWeUse = GetMetaFromSomewhere(); + IMatrixMap completeMap = GetCompleteMapFromSomewhere(); + + DataIndexer indexer = new(completeMap, meta); + Matrix output = new(meta, new DecimalDataValue[indexer.DataLength]); + using Stream fileStream = File.OpenRead(PATH_TO_PX_FILE); + using PxFileStreamDataReader dataReader = new(fileStream); + dataReader.ReadDecimalDataValues(output.Data, 0, indexer); +``` + +### Metadata models +#### ```Matrix``` +Consists of ```IReadOnlyMatrixMetadata Metadata``` and ```TData[] Data```. This is the topmost level of the structure. The order of the data points is determined by the metadata in the following way: + +| dim0-val0 || dim0-val1 || dim0-val2 || +|-----------|-----------|-----------|-----------|-----------|-----------| +| dim1-val0 | dim1-val1| dim1-val0 | dim1-val1| dim1-val0 | dim1-val1| +| dim2-val0 | dim2-val0| dim2-val0 | dim2-val0| dim2-val0 | dim2-val0| +| 0 | 1 | 2 | 3 | 4 | 5 | + +In this example, the dim0 is the first dimension in the dimension list of the metadata. And val0 is the first value in each dimension. Dim2 has only one value, so it does not affect the order of the data points (in other words if the length of the dimension is one, it has no impact on the volume of the data cube). +Example: If we choose the second value from dim0 (index 1) and the first value from dim1 (index 0), the data index is 2 (We can only choose val0 from dim2). + +There are no limits for the number or size of dimensions. But it is important to note, that if even one dimension has a size of 0, the data array will be empty (the volume of the data cube will be 0). + +```GetTransform(IMatrixMap map)``` method can be used to take a subset of the the matrix and/or change the order of the dimensions or the dimension values. +It creates a new mutable deep copy of the matrix that have the structure defined by the map parameter. The data array will also be copied and reordered based on the map. + +#### ```MatrixMap : IMatrixMap``` +This is a minimal way to represent the structure of the metadata. Does not contain any other information than the dimension and dimension value codes. +The ```IReadOnlyMatrixMetadata``` also implements the ```IMatrixMap``` interface. + +#### ```MatrixMetadata : IReadOnlyMatrixMetadata``` +Represents the table level metadata of the px file. Contains the dimension list and the language information. +The ```IReadOnlyMatrixMetadata``` is a read-only interface for the metadata, this should be the primary way to access and use the metadata. + +Similar to the ```Matrix```, the ```IReadOnlyMatrixMetadata``` has a ```GetTransform(IMatrixMap map)``` method that can be used to create a mutable deep copy with the structure defined by the map parameter. + +#### ```Dimension : IReadOnlyDimension``` +Represents the dimension level metadata of a px-file. This is a base class for all dimensions, and all dimension in the ```MatrixMetadata``` are in this type. +The dimensions have a ```Type``` property and dimensions with type ```Content``` or ```Time``` have some additional properties that can be accessed through type casting. + +Each dimension has a unique string code among the dimension in the matrix. + +Beacause the content dimension has its own type for dimension values, the dimensions hold their values in ```ValueList``` or ```ContentValueList``` collections to make it compatible with the C# type system. +Those collections have their own methods for accessing and going through the values (```Map()```, ```Find()```). +They both implement the ```IReadOnlyList``` interface, but that does not allow accessing the values as mutable or as content dimension values. + +#### ```ContentDimension : Dimension``` +Differs from the base class by having values of type ```ContentDimensionValue```. + +#### ```TimeDimension : Dimension``` +Shares the same value type as the base class, but has additional properties in the dimension level metadata. + +#### ```DimensionValue : IReadOnlyDimensionValue``` +Represents the dimension value level metadata of a px-file. This is a base class for all dimension values. Each value has a unique string code among the values in the dimension. + +#### ```ContentDimensionValue : DimensionValue``` +Dimension value that contais content dimension value specific metadata. + +#### ```MetaProperty``` +Px.Utils supports reading any metadata properties that follow the px file syntax. The properties are stored in a ```Dictionary``` collection called ```AdditionalProperties``` where the dictionary key is the property keyword. +The base class ```MetaProperty``` is abstract and each supported property type has its own class that inherits from it. + +### Data models +```IDataValue``` is an interface for the data points that defines the basic computation methods for the data points. See the Computing section for more information. +If the ```TData``` in the ```Matrix``` implements the ```IDataValue``` interface, all of the extension methods for computing can be used. Other than that, the ```TData``` has no constraints. + +There are two structs in Px.Utils that implement the ```IDataValue``` interface: ```DoubleDataValue``` and ```DecimalDataValue```. These structs can be used to store data values as doubles or decimals, but they also enable handling of the missing data values. +Px.Utils provides functionality for reading the data as these structs from the px files. + +Standard numeric value types can be used as the ```TData``` in the ```Matrix``` class. However, the user must then provide the enconding for the missing data values. ### Validation -#### Px file validation -Px files can be validated either as a whole by using PxFileValidator or by using individual validators - SyntaxValidator, ContentValidator and DataValidator - for different parts of the file. Custom validation functions or validator classes can be added to the validation processes. -Validator classes implement either IPxFileStreamValidator or IPxFileStreamValidatorAsync interfaces for synchronous and asynchronous validation processes respectively. Custom validation functions must implement either the IPxFileValidator or IPxFileValidatorAsync, IValidator or IValidatorAsync interfaces. -IPxFileStreamValidator and IPxFileStreamValidatorAsync interfaces required the following parameters to run their Validate or ValidateAsync functions: +Px files can be validated either as a whole by using ```PxFileValidator``` or by using individual validators - ```SyntaxValidator```, ```ContentValidator``` and ```DataValidator``` - for different parts of the file. Custom validation functions or validator classes can be added to the validation processes. +Validator classes implement either ```IPxFileStreamValidator``` or ```IPxFileStreamValidatorAsync``` interfaces for synchronous and asynchronous validation processes respectively. Custom validation functions must implement either the ```IPxFileValidator``` or ```IPxFileValidatorAsync```, ```IValidator``` or ```IValidatorAsync``` interfaces. +```IPxFileStreamValidator``` and ```IPxFileStreamValidatorAsync``` interfaces required the following parameters to run their ```Validate()``` or ```ValidateAsync()``` functions: - stream (Stream): The stream of the px file to be validated - filename (string): Name of the file to be validated - encoding (Encoding, optional): Encoding of the px file. Default is Encoding.Default - fileSystem (IFileSystem, optional): Object that defines the file system used for the validation process. Default file called LocalFileSystem system is used if none provided. -##### PxFileValidator (IPxFileStreamValidator, IPxFileStreamValidatorAsync) -PxFileValidator is a class that validates the whole px file including its data, metadata syntax and metadata contents. The class can be instantiated with the following parameters: +#### PxFileValidator : IPxFileStreamValidator, IPxFileStreamValidatorAsync +```PxFileValidator``` is a class that validates the whole px file including its data, metadata syntax and metadata contents. The class can be instantiated with the following parameters: - syntaxConf (PxFileSyntaxConf, optional): Object that contains px file syntax configuration tokens and symbols. Custom validator objects can be injected by calling the SetCustomValidatorFunctions or SetCustomValidators methods of the PxFileValidator object. Custom validators must implement either the IPxFileValidator or IPxFileValidatorAsync interface. Custom validation methods are stored in CustomSyntaxValidationFunctions and CustomContentValidationFunctions objects for syntax and content validation processes respectively. Once the PxFileValidator object is instantiated, either the Validate or ValidateAsync method can be called to validate the px file. The Validate method returns a ValidationResult object that contains the validation results as a key value pair containing information about the rule violations. -##### SyntaxValidator -SyntaxValidator is a class that validates the syntax of a px file's metadata. It needs to be run before other validators, because both the ContentValidator and DataValidator require information from the SyntaxValidationResult object that SyntaxValidator Validate and ValidateAsync methods return. +#### SyntaxValidator : IPxFileStreamValidator, IPxFileStreamValidatorAsync +```SyntaxValidator``` is a class that validates the syntax of a px file's metadata. It needs to be run before other validators, because both the ```ContentValidator``` and ```DataValidator``` require information from the ```SyntaxValidationResult``` object that ```SyntaxValidator``` ```Validate()``` and ```ValidateAsync()``` methods return. The class can be instantiated with the following parameters: - syntaxConf (PxFileSyntaxConf, optional): Object that contains px file syntax configuration tokens and symbols. - customValidationFunctions (CustomSyntaxValidationFunctions, optional): Object that contains custom validation functions for the syntax validation process. -##### ContentValidator -ContentValidator class validates the integrity of the contents of a px file's metadata. It needs to be run after the SyntaxValidator, because it requires information from the SyntaxValidationResult object that SyntaxValidator Validate and ValidateAsync methods return. +#### ContentValidator : IValidator +```ContentValidator``` class validates the integrity of the contents of a px file's metadata. It needs to be run after the ```SyntaxValidator```, because it requires information from the ```SyntaxValidationResult``` object that ```SyntaxValidator``` ```Validate()``` and ```ValidateAsync()``` methods return. The class can be instantiated with the following parameters: - filename (string): Name of the file to be validated - encoding (Encoding): Encoding of the px file. @@ -70,16 +165,16 @@ The class can be instantiated with the following parameters: - customContentValidationFunctions (CustomContentValidationFunctions, optional): Object that contains custom functions for validating the px file metadata contents. - syntaxConf (PxFileSyntaxConf, optional): Object that contains px file syntax configuration tokens and symbols. -##### DataValidator -DataValidator class is used to validate the data section of a px file. It needs to be run after the SyntaxValidator, because it requires information from both the SyntaxValidationResult and ContentValidationResult objects that SyntaxValidator and ContentValidator Validate and ValidateAsync methods return. +#### DataValidator : IPxFileStreamValidator, IPxFileStreamValidatorAsync +```DataValidator``` class is used to validate the data section of a px file. It needs to be run after the ```SyntaxValidator```, because it requires information from both the ```SyntaxValidationResult``` and ```ContentValidationResult``` objects that ```SyntaxValidator``` and ```ContentValidator``` ```Validate()``` and ```ValidateAsync()``` methods return. The class can be instantiated with the following parameters: - rowLen (int): Length of one row of Px file data. ContentValidationResult object contains this information. - numOfRows (int): Amount of rows of Px file data. This information is also stored in ContentValidationResult object. - startRow (long): The row number where the data section starts. This information is stored in the SyntaxValidationResult object. - conf (PxFileSyntaxConf, optional): Syntax configuration for the Px file -#### Database validation -Whole px file databases can be validated using DatabaseValidator class. Validation can be done by using the blocking Validate or asynchronous ValidateAsync methods. DatabaseValidator class can be instantiated using the following parameters: +#### DatabaseValidator : IValidator, IValidatorAsync +Whole px file databases can be validated using ```DatabaseValidator``` class. Validation can be done by using the blocking ```Validate()``` or asynchronous ```ValidateAsync()``` methods. ```DatabaseValidator``` class can be instantiated using the following parameters: - directoryPath (string): Path to the database root - syntaxConf (PxFileSyntaxConf, optional): Syntax configuration for the Px file - fileSystem (IFileSystem, optional): Object that defines the file system used for the validation process. Default file system is used if none provided @@ -87,43 +182,40 @@ Whole px file databases can be validated using DatabaseValidator class. Validati - customAliasFileValidators (IDatabaseValidator, optional): Object containing validator functions ran for each alias file within the database - customDirectoryValidators (IDatabaseValidator, optional): Object containing validator functions ran for each subdirectory within the database -Database validation process validates each px file within the database and also the required structure and consistency of the database languages and encoding formats. The return object is a ValidationResult object that contains ValidationFeedback objects gathered during the validation process. +Database validation process validates each px file within the database and also the required structure and consistency of the database languages and encoding formats. The return object is a ```ValidationResult``` object that contains ```ValidationFeedback``` objects gathered during the validation process. The database needs to contain alias files for each language used in the database for each folder that contains either subcategory folders or px files. If either languages or encoding formats differ between alias or px files, warnings are generated. -### Data models -TBA - ### Computing ```Matrix``` class has a set of extension methods for performing basic computations for the datapoints. #### Sum -```SumToNewValue``` computes sums of datapoints defined by a subset of values from a given dimension. +```SumToNewValue()``` computes sums of datapoints defined by a subset of values from a given dimension. The method takes a new dimension value as a parameter that will define the resulting values. -The method also has an asyncronous variant ```SumToNewValueAsync```. +The method also has an asyncronous variant ```SumToNewValueAsync()```. -```AddConstantToSubset``` adds a constant to a subset of datapoints. Also has an asynchronous variant ```AddConstantToSubsetAsync```. +```AddConstantToSubset()``` adds a constant to a subset of datapoints. Also has an asynchronous variant ```AddConstantToSubsetAsync()```. #### Multiplication -```MultiplyToNewValue``` computes products of datapoints defined by a subset of values from a given dimension. +```MultiplyToNewValue()``` computes products of datapoints defined by a subset of values from a given dimension. The method takes a new dimension value as a parameter that will define the resulting values. -The method also has an asyncronous variant ```MultiplyToNewValueAsync```. +The method also has an asyncronous variant ```MultiplyToNewValueAsync()```. -```MultiplySubsetByConstant``` Multiply a subset of datapoints by a constant. Also has an asynchronous variant ```MultiplySubsetByConstantAsync```. +```MultiplySubsetByConstant()``` Multiply a subset of datapoints by a constant. Also has an asynchronous variant ```MultiplySubsetByConstantAsync()```. #### Division -```DivideSubsetBySelectedValue``` divides a subset of datapoints defined by values from one dimension with datapoints defined by a value from the same dimension. -Also has an asyncronous variant ```DivideSubsetBySelectedValueAsync``` +```DivideSubsetBySelectedValue()``` divides a subset of datapoints defined by values from one dimension with datapoints defined by a value from the same dimension. +Also has an asyncronous variant ```DivideSubsetBySelectedValueAsync()``` -```DivideSubsetByConstant``` Divide a subset of datapoints by a constant. Also has an asynchronous variant ```DivideSubsetByConstantAsync```. +```DivideSubsetByConstant()``` Divide a subset of datapoints by a constant. Also has an asynchronous variant ```DivideSubsetByConstantAsync()```. #### General -```ApplyOverDimension``` Generatas a new set datapoints by applying a function to datapoints defined by a subset of values from one dimension. +```ApplyOverDimension()``` Generatas a new set datapoints by applying a function to datapoints defined by a subset of values from one dimension. The method takes a new dimension value as a parameter that will define the resulting values. -```ApplyToSubMap``` Applies a function to a set of datapoints. +```ApplyToSubMap()``` Applies a function to a set of datapoints. -```ApplyRelative``` Applies a function to a set of datapoints defined by a subset of values from one dimension. +```ApplyRelative()``` Applies a function to a set of datapoints defined by a subset of values from one dimension. The method also takes a code of a value from the same dimension as a parameter which is used to define additional datapoints to be used as an input for the function.