diff --git a/MatFileHandler.Tests/MatFileReaderTests.cs b/MatFileHandler.Tests/MatFileReaderTests.cs index 88fd0e8..b317cf3 100755 --- a/MatFileHandler.Tests/MatFileReaderTests.cs +++ b/MatFileHandler.Tests/MatFileReaderTests.cs @@ -1,5 +1,6 @@ // Copyright 2017-2018 Alexander Luzgarev +using System; using System.IO; using System.Linq; using System.Numerics; @@ -388,6 +389,66 @@ namespace MatFileHandler.Tests Assert.That(ppp["y"].ConvertToDoubleArray(), Is.EquivalentTo(new[] { 200.0 })); } + /// + /// Test datetime objects. + /// + [Test] + public void TestDatetime() + { + var matFile = GetTests("good")["datetime"]; + var d = matFile["d"].Value as IMatObject; + var datetime = new DatetimeAdapter(d); + Assert.That(datetime.Dimensions, Is.EquivalentTo(new[] { 1, 2 })); + Assert.That(datetime[0], Is.EqualTo(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero))); + Assert.That(datetime[1], Is.EqualTo(new DateTimeOffset(1987, 1, 2, 3, 4, 5, TimeSpan.Zero))); + } + + /// + /// Another test for datetime objects. + /// + [Test] + public void TestDatetime2() + { + var matFile = GetTests("good")["datetime2"]; + var d = matFile["d"].Value as IMatObject; + var datetime = new DatetimeAdapter(d); + Assert.That(datetime.Dimensions, Is.EquivalentTo(new[] { 1, 1 })); + var diff = new DateTimeOffset(2, 1, 1, 1, 1, 1, 235, TimeSpan.Zero); + Assert.That(datetime[0] - diff < TimeSpan.FromMilliseconds(1)); + Assert.That(diff - datetime[0] < TimeSpan.FromMilliseconds(1)); + } + + /// + /// Test string objects. + /// + [Test] + public void TestString() + { + var matFile = GetTests("good")["string"]; + var s = matFile["s"].Value as IMatObject; + var str = new StringAdapter(s); + Assert.That(str.Dimensions, Is.EquivalentTo(new[] { 4, 1 })); + Assert.That(str[0], Is.EqualTo("abc")); + Assert.That(str[1], Is.EqualTo("defgh")); + Assert.That(str[2], Is.EqualTo("абвгд")); + Assert.That(str[3], Is.EqualTo("æøå")); + } + + /// + /// Test duration objects. + /// + [Test] + public void TestDuration() + { + var matFile = GetTests("good")["duration"]; + var d = matFile["d"].Value as IMatObject; + var duration = new DurationAdapter(d); + Assert.That(duration.Dimensions, Is.EquivalentTo(new[] { 1, 3 })); + Assert.That(duration[0], Is.EqualTo(TimeSpan.FromTicks(12345678L))); + Assert.That(duration[1], Is.EqualTo(new TimeSpan(0, 2, 4))); + Assert.That(duration[2], Is.EqualTo(new TimeSpan(1, 3, 5))); + } + private static AbstractTestDataFactory GetTests(string factoryName) => new MatTestDataFactory(Path.Combine(TestDirectory, factoryName)); diff --git a/MatFileHandler.Tests/test-data/good/datetime.mat b/MatFileHandler.Tests/test-data/good/datetime.mat new file mode 100644 index 0000000..6f6899f Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/datetime.mat differ diff --git a/MatFileHandler.Tests/test-data/good/datetime2.mat b/MatFileHandler.Tests/test-data/good/datetime2.mat new file mode 100644 index 0000000..d9d19c5 Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/datetime2.mat differ diff --git a/MatFileHandler.Tests/test-data/good/duration.mat b/MatFileHandler.Tests/test-data/good/duration.mat new file mode 100644 index 0000000..162bbe5 Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/duration.mat differ diff --git a/MatFileHandler.Tests/test-data/good/string.mat b/MatFileHandler.Tests/test-data/good/string.mat new file mode 100644 index 0000000..b86a9c9 Binary files /dev/null and b/MatFileHandler.Tests/test-data/good/string.mat differ diff --git a/MatFileHandler/DatetimeAdapter.cs b/MatFileHandler/DatetimeAdapter.cs new file mode 100644 index 0000000..c86371f --- /dev/null +++ b/MatFileHandler/DatetimeAdapter.cs @@ -0,0 +1,62 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; +using System.Linq; +using System.Numerics; + +namespace MatFileHandler +{ + /// + /// A better interface for using datetime objects. + /// + public class DatetimeAdapter + { + private readonly double[] data; + private readonly double[] data2; + private readonly int[] dimensions; + + private readonly DateTimeOffset epoch; + + /// + /// Initializes a new instance of the class. + /// + /// Source datetime object. + public DatetimeAdapter(IArray array) + { + var matObject = array as IMatObject; + if (matObject?.ClassName != "datetime") + { + throw new ArgumentException("The object provided is not a datetime."); + } + + epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var dataArray = matObject["data", 0] as IArrayOf; + if (dataArray is null) + { + var dataComplex = matObject["data", 0] as IArrayOf; + var complexData = dataComplex.ConvertToComplexArray(); + data = complexData.Select(c => c.Real).ToArray(); + data2 = complexData.Select(c => c.Imaginary).ToArray(); + dimensions = dataComplex.Dimensions; + } + else + { + data = dataArray.ConvertToDoubleArray(); + data2 = new double[data.Length]; + dimensions = dataArray.Dimensions; + } + } + + /// + /// Gets datetime array dimensions. + /// + public int[] Dimensions => dimensions; + + /// + /// Gets values of datetime object at given position in the array converted to . + /// + /// Indices. + /// Value converted to . + public DateTimeOffset this[params int[] list] => epoch.AddMilliseconds(data[Dimensions.DimFlatten(list)]); + } +} \ No newline at end of file diff --git a/MatFileHandler/DurationAdapter.cs b/MatFileHandler/DurationAdapter.cs new file mode 100644 index 0000000..c7a6a35 --- /dev/null +++ b/MatFileHandler/DurationAdapter.cs @@ -0,0 +1,44 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; + +namespace MatFileHandler +{ + /// + /// A better interface for using duration objects. + /// + public class DurationAdapter + { + private readonly int[] dimensions; + private readonly double[] data; + + /// + /// Initializes a new instance of the class. + /// + /// Source duration object. + public DurationAdapter(IArray array) + { + var matObject = array as IMatObject; + if (matObject?.ClassName != "duration") + { + throw new ArgumentException("The object provided is not a duration."); + } + + var dataObject = matObject["millis", 0]; + data = dataObject.ConvertToDoubleArray(); + dimensions = dataObject.Dimensions; + } + + /// + /// Gets duration array dimensions. + /// + public int[] Dimensions => dimensions; + + /// + /// Gets duration object at given position. + /// + /// Indices. + /// Value. + public TimeSpan this[params int[] list] => TimeSpan.FromTicks((long)(10000.0 * data[Dimensions.DimFlatten(list)])); + } +} \ No newline at end of file diff --git a/MatFileHandler/MatFileHandler.csproj b/MatFileHandler/MatFileHandler.csproj index ebd7993..7152830 100755 --- a/MatFileHandler/MatFileHandler.csproj +++ b/MatFileHandler/MatFileHandler.csproj @@ -1,7 +1,7 @@  netstandard2.0;net461 - 1.3.0-beta2 + 1.3.0-beta3 MatFileHandler A library for reading and writing MATLAB .mat files. Alexander Luzgarev diff --git a/MatFileHandler/OpaqueLink.cs b/MatFileHandler/OpaqueLink.cs index fcb0cfe..ed71d2f 100644 --- a/MatFileHandler/OpaqueLink.cs +++ b/MatFileHandler/OpaqueLink.cs @@ -107,8 +107,8 @@ namespace MatFileHandler return false; } - var objectPosition = IndexToObjectId[index]; - var objectInfo = subsystemData.ObjectInformation.First(pair => pair.Value.Position == objectPosition).Value; + var objectId = IndexToObjectId[index]; + var objectInfo = subsystemData.ObjectInformation[objectId]; var fieldId = objectInfo.FieldLinks[fieldIndex]; output = subsystemData.Data[fieldId]; return true; diff --git a/MatFileHandler/StringAdapter.cs b/MatFileHandler/StringAdapter.cs new file mode 100644 index 0000000..afc5f80 --- /dev/null +++ b/MatFileHandler/StringAdapter.cs @@ -0,0 +1,78 @@ +// Copyright 2017-2018 Alexander Luzgarev + +using System; +using System.Text; + +namespace MatFileHandler +{ + /// + /// A better interface for using string objects. + /// + public class StringAdapter + { + private readonly int[] dimensions; + private readonly string[] strings; + + /// + /// Initializes a new instance of the class. + /// + /// Source string object. + public StringAdapter(IArray array) + { + var matObject = array as IMatObject; + if (matObject?.ClassName != "string") + { + throw new ArgumentException("The object provided is not a string."); + } + + var binaryData = matObject["any", 0] as IArrayOf; + + (dimensions, strings) = ParseBinaryData(binaryData.Data); + } + + /// + /// Gets string array dimensions. + /// + public int[] Dimensions => dimensions; + + /// + /// Gets string object at given position. + /// + /// Indices. + /// Value. + public string this[params int[] list] => strings[Dimensions.DimFlatten(list)]; + + private static (int[] dimensions, string[] strings) ParseBinaryData(ulong[] binaryData) + { + var numberOfDimensions = (int)binaryData[1]; + var d = new int[numberOfDimensions]; + for (var i = 0; i < numberOfDimensions; i++) + { + d[i] = (int)binaryData[i + 2]; + } + + var numberOfElements = d.NumberOfElements(); + var start = numberOfDimensions + 2; + var lengths = new int[numberOfElements]; + for (var i = 0; i < numberOfElements; i++) + { + lengths[i] = (int)binaryData[start + i]; + } + + var strings = new string[numberOfElements]; + + start += numberOfElements; + var numberOfUlongsLeft = binaryData.Length - start; + var bytes = new byte[numberOfUlongsLeft * sizeof(ulong)]; + Buffer.BlockCopy(binaryData, start * sizeof(ulong), bytes, 0, bytes.Length); + var counter = 0; + for (var i = 0; i < numberOfElements; i++) + { + strings[i] = Encoding.Unicode.GetString(bytes, counter * 2, lengths[i] * 2); + counter += lengths[i]; + } + + return (d, strings); + } + } +} \ No newline at end of file diff --git a/MatFileHandler/SubsystemData.cs b/MatFileHandler/SubsystemData.cs index ab65292..fd987b8 100644 --- a/MatFileHandler/SubsystemData.cs +++ b/MatFileHandler/SubsystemData.cs @@ -125,11 +125,9 @@ namespace MatFileHandler /// /// Initializes a new instance of the class. /// - /// Position of object in the object information table. /// A dictionary mapping the field indices to "field values" indices. - public ObjectInfo(int position, Dictionary fieldLinks) + public ObjectInfo(Dictionary fieldLinks) { - Position = position; this.fieldLinks = fieldLinks ?? throw new ArgumentNullException(nameof(fieldLinks)); } @@ -137,11 +135,6 @@ namespace MatFileHandler /// Gets mapping between the field indices and "field values" indices. /// public IReadOnlyDictionary FieldLinks => fieldLinks; - - /// - /// Gets position of object in the object information table. - /// - public int Position { get; } } } } \ No newline at end of file diff --git a/MatFileHandler/SubsystemDataReader.cs b/MatFileHandler/SubsystemDataReader.cs index ce7f18b..9b1b6fc 100644 --- a/MatFileHandler/SubsystemDataReader.cs +++ b/MatFileHandler/SubsystemDataReader.cs @@ -50,8 +50,19 @@ namespace MatFileHandler } } + var numberOfEmbeddedObjects = (offsets[4] - offsets[3] - 8) / 16; + Dictionary> embeddedObjectPositionsToValues = null; + using (var stream = new MemoryStream(info, offsets[3], offsets[4] - offsets[3])) + { + using (var reader = new BinaryReader(stream)) + { + embeddedObjectPositionsToValues = + ReadEmbeddedObjectPositionsToValuesMapping(reader, numberOfEmbeddedObjects); + } + } + var numberOfObjects = ((offsets[5] - offsets[4]) / 24) - 1; - Dictionary objectClasses = null; + Dictionary objectClasses = null; using (var stream = new MemoryStream(info, offsets[4], offsets[5] - offsets[4])) { using (var reader = new BinaryReader(stream)) @@ -60,12 +71,14 @@ namespace MatFileHandler } } - Dictionary> objectToFields = null; + var numberOfObjectPositions = objectClasses.Values.Count(x => x.ObjectPosition != 0); + + Dictionary> objectPositionsToValues = null; using (var stream = new MemoryStream(info, offsets[5], offsets[6] - offsets[5])) { using (var reader = new BinaryReader(stream)) { - objectToFields = ReadObjectToFieldsMapping(reader, numberOfObjects); + objectPositionsToValues = ReadObjectPositionsToValuesMapping(reader, numberOfObjectPositions); } } @@ -74,7 +87,8 @@ namespace MatFileHandler classIdToName, fieldNames, objectClasses, - objectToFields); + objectPositionsToValues, + embeddedObjectPositionsToValues); var allFields = objectInformation.Values.SelectMany(obj => obj.FieldLinks.Values); var data = new Dictionary(); @@ -82,120 +96,77 @@ namespace MatFileHandler { data[i] = TransformOpaqueData(opaqueData[i + 2], subsystemData); } + return new SubsystemData(classInformation, objectInformation, data); } - private static IArray TransformOpaqueData(IArray array, SubsystemData subsystemData) - { - if (array is MatNumericalArrayOf uintArray) - { - if (uintArray.Data[0] == 3707764736u) - { - var (dimensions, indexToObjectId, classIndex) = DataElementReader.ParseOpaqueData(uintArray.Data); - return new OpaqueLink( - uintArray.Name, - string.Empty, - string.Empty, - dimensions, - array as DataElement, - indexToObjectId, - classIndex, - subsystemData); - } - } - - return array; - } - - private static Dictionary ReadObjectClasses(BinaryReader reader, int numberOfObjects) - { - var result = new Dictionary(); - reader.ReadBytes(24); - for (var i = 0; i < numberOfObjects; i++) - { - var classId = reader.ReadInt32(); - reader.ReadBytes(12); - var objectPosition = reader.ReadInt32(); - var objectId = reader.ReadInt32(); - result[objectPosition] = (objectId, classId); - } - - return result; - } - - private static (Dictionary, Dictionary) GatherClassAndObjectInformation( - Dictionary classIdToName, - string[] fieldNames, - Dictionary objectClasses, - Dictionary> objectToFields) + private static (Dictionary, Dictionary) + GatherClassAndObjectInformation( + Dictionary classIdToName, + string[] fieldNames, + Dictionary objectClasses, + Dictionary> objectPositionsToValues, + Dictionary> embeddedObjectPositionsToValues) { var classInfos = new Dictionary(); foreach (var classId in classIdToName.Keys) { var className = classIdToName[classId]; var fieldIds = new SortedSet(); - foreach (var objectPosition in objectToFields.Keys) + foreach (var objectPosition in objectPositionsToValues.Keys) { - var (_, thisObjectClassId) = objectClasses[objectPosition]; - if (thisObjectClassId != classId) + var keyValuePair = objectClasses.First(pair => pair.Value.ObjectPosition == objectPosition); + if (keyValuePair.Value.ClassId != classId) { continue; } - foreach (var fieldId in objectToFields[objectPosition].Keys) + foreach (var fieldId in objectPositionsToValues[objectPosition].Keys) { fieldIds.Add(fieldId); } } + + foreach (var objectPosition in embeddedObjectPositionsToValues.Keys) + { + var keyValuePair = objectClasses.First(pair => pair.Value.EmbeddedObjectPosition == objectPosition); + if (keyValuePair.Value.ClassId != classId) + { + continue; + } + + foreach (var fieldId in embeddedObjectPositionsToValues[objectPosition].Keys) + { + fieldIds.Add(fieldId); + } + } + var fieldToIndex = new Dictionary(); foreach (var fieldId in fieldIds) { fieldToIndex[fieldNames[fieldId - 1]] = fieldId; } + classInfos[classId] = new SubsystemData.ClassInfo(className, fieldToIndex); } var objectInfos = new Dictionary(); - foreach (var objectPosition in objectToFields.Keys) + foreach (var objectPosition in objectPositionsToValues.Keys) { - var (objectId, _) = objectClasses[objectPosition]; - objectInfos[objectId] = new SubsystemData.ObjectInfo(objectPosition, objectToFields[objectPosition]); + var keyValuePair = objectClasses.First(pair => pair.Value.ObjectPosition == objectPosition); + objectInfos[keyValuePair.Key] = new SubsystemData.ObjectInfo(objectPositionsToValues[objectPosition]); + } + + foreach (var objectPosition in embeddedObjectPositionsToValues.Keys) + { + var keyValuePair = objectClasses.First(pair => pair.Value.EmbeddedObjectPosition == objectPosition); + objectInfos[keyValuePair.Key] = + new SubsystemData.ObjectInfo(embeddedObjectPositionsToValues[objectPosition]); } return (classInfos, objectInfos); } - private static Dictionary ReadFieldToFieldDataMapping(BinaryReader reader) - { - var length = reader.ReadInt32(); - var result = new Dictionary(); - for (var i = 0; i < length; i++) - { - var x = reader.ReadInt32(); - var y = reader.ReadInt32(); - var index = x * y; - var link = reader.ReadInt32(); - result[index] = link; - } - return result; - } - - private static Dictionary> ReadObjectToFieldsMapping(BinaryReader reader, int numberOfObjects) - { - var result = new Dictionary>(); - reader.ReadBytes(8); - for (var objectPosition = 1; objectPosition <= numberOfObjects; objectPosition++) - { - result[objectPosition] = ReadFieldToFieldDataMapping(reader); - var position = reader.BaseStream.Position; - if (position % 8 != 0) - { - reader.ReadBytes(8 - (int)(position % 8)); - } - } - return result; - } - private static Dictionary ReadClassNames( BinaryReader reader, string[] fieldNames, @@ -219,6 +190,98 @@ namespace MatFileHandler return result; } + private static Dictionary> ReadEmbeddedObjectPositionsToValuesMapping( + BinaryReader reader, int numberOfObjects) + { + var result = new Dictionary>(); + reader.ReadBytes(8); + for (var objectPosition = 1; objectPosition <= numberOfObjects; objectPosition++) + { + var a = reader.ReadInt32(); + var fieldIndex = reader.ReadInt32(); + var c = reader.ReadInt32(); + var valueIndex = reader.ReadInt32(); + result[objectPosition] = new Dictionary { [fieldIndex] = valueIndex }; + } + + return result; + } + + private static string[] ReadFieldNames(byte[] bytes, int startPosition, int numberOfFields) + { + var result = new string[numberOfFields]; + var position = startPosition; + for (var i = 0; i < numberOfFields; i++) + { + var list = new List(); + while (bytes[position] != 0) + { + list.Add(bytes[position]); + position++; + } + + result[i] = Encoding.ASCII.GetString(list.ToArray()); + position++; + } + + return result; + } + + private static Dictionary ReadFieldToFieldDataMapping(BinaryReader reader) + { + var length = reader.ReadInt32(); + var result = new Dictionary(); + for (var i = 0; i < length; i++) + { + var x = reader.ReadInt32(); + var y = reader.ReadInt32(); + var index = x * y; + var link = reader.ReadInt32(); + result[index] = link; + } + + return result; + } + + private static Dictionary ReadObjectClasses( + BinaryReader reader, + int numberOfObjects) + { + var result = new Dictionary(); + reader.ReadBytes(24); + for (var i = 0; i < numberOfObjects; i++) + { + var classId = reader.ReadInt32(); + reader.ReadBytes(8); + var embeddedObjectPosition = reader.ReadInt32(); + var objectPosition = reader.ReadInt32(); + var loadingOrder = reader.ReadInt32(); + result[i + 1] = + new ObjectClassInformation(embeddedObjectPosition, objectPosition, loadingOrder, classId); + } + + return result; + } + + private static Dictionary> ReadObjectPositionsToValuesMapping( + BinaryReader reader, + int numberOfObjects) + { + var result = new Dictionary>(); + reader.ReadBytes(8); + for (var objectPosition = 1; objectPosition <= numberOfObjects; objectPosition++) + { + result[objectPosition] = ReadFieldToFieldDataMapping(reader); + var position = reader.BaseStream.Position; + if (position % 8 != 0) + { + reader.ReadBytes(8 - (int)(position % 8)); + } + } + + return result; + } + private static (int[] offsets, int newPosition) ReadOffsets(byte[] bytes, int startPosition) { var position = startPosition; @@ -236,29 +299,52 @@ namespace MatFileHandler break; } + offsets.Add(next); } return (offsets.ToArray(), position); } - private static string[] ReadFieldNames(byte[] bytes, int startPosition, int numberOfFields) + private static IArray TransformOpaqueData(IArray array, SubsystemData subsystemData) { - var result = new string[numberOfFields]; - var position = startPosition; - for (var i = 0; i < numberOfFields; i++) + if (array is MatNumericalArrayOf uintArray) { - var list = new List(); - while (bytes[position] != 0) + if (uintArray.Data[0] == 3707764736u) { - list.Add(bytes[position]); - position++; + var (dimensions, indexToObjectId, classIndex) = DataElementReader.ParseOpaqueData(uintArray.Data); + return new OpaqueLink( + uintArray.Name, + string.Empty, + string.Empty, + dimensions, + array as DataElement, + indexToObjectId, + classIndex, + subsystemData); } - result[i] = Encoding.ASCII.GetString(list.ToArray()); - position++; } - return result; + return array; + } + + private struct ObjectClassInformation + { + public ObjectClassInformation(int embeddedObjectPosition, int objectPosition, int loadingOrder, int classId) + { + EmbeddedObjectPosition = embeddedObjectPosition; + ObjectPosition = objectPosition; + LoadingOrder = loadingOrder; + ClassId = classId; + } + + public int ClassId { get; } + + public int EmbeddedObjectPosition { get; } + + public int LoadingOrder { get; } + + public int ObjectPosition { get; } } } -} +} \ No newline at end of file