Compare commits

...

86 Commits

Author SHA1 Message Date
366e8e8ead Merge pull request 'Even better objects' (#39) from dev/even-better-objects into master 2025-04-14 19:03:42 +00:00
c8acc1f93d Remove BOM from .cs files 2025-04-14 20:22:48 +02:00
003186b3b1 Version bump 2025-04-14 18:37:29 +02:00
6c95ea7a4a Update links 2025-04-14 18:36:39 +02:00
d24f3c7250 More file-scoped namespaces 2025-04-14 18:24:34 +02:00
6672ae4d24 Get rid of file headers 2025-04-13 17:44:13 +02:00
919014a109 Detect end of stream better 2025-04-12 11:40:22 +02:00
1822e75fd8 Make tests less oopy 2025-04-12 09:07:16 +02:00
d87cc2c99c Fix all the warnings 2025-04-12 09:06:12 +02:00
306992f969 Enable .NET analyzers 2025-04-08 16:50:37 +02:00
5080f35e69 Even better handling of embedded objects 2025-04-08 16:19:47 +02:00
Rob Hague
cc3bdc000c
Fix release notes link (#38)
* Fix release notes link

* link to latest
2025-04-06 16:27:01 +02:00
841ac432a5
Version bump 2025-04-06 12:52:04 +02:00
489d93eff1
Merge pull request #36 from mahalex/dev/better-writer
Better MatFileWriter
2025-04-06 12:46:33 +02:00
dcfc685b62 Test on net461 2025-04-06 12:42:50 +02:00
e9582723c9 Reduce memory consumption of MatFileWriter 2025-04-06 12:42:50 +02:00
3ae8f06b3e
Merge pull request #34 from Rob-Hague/substream
Read compressed elements without loading into memory
2025-04-06 12:27:16 +02:00
a3602f80b4
Add README to package
Add README to package
2025-04-06 10:25:45 +02:00
Robert Hague
1eecbf2a79 Add README to package 2025-04-05 13:07:41 +02:00
Robert Hague
e8bf3f89ee Read compressed elements without loading into memory 2025-04-05 12:29:25 +02:00
Robert Hague
37feeb5863 Add test coverage and fix for reading unseekable or unaligned streams 2025-04-05 11:51:23 +02:00
7494502062
Better treatment of embedded objects 2025-04-03 18:50:27 +02:00
6069d16c07 Better treatment of embedded objects 2025-04-03 18:33:03 +02:00
f56508c3ad
Merge pull request #30 from mahalex/dev/enums
Support for enumerations
2025-03-15 09:25:59 +01:00
f342abab7b Bump version & year in LICENSE 2025-03-15 09:06:13 +01:00
106c839b10 Get rid of analyzers in test project 2025-03-15 09:06:13 +01:00
5a2226629a Implement EnumAdapter
#17
2025-03-15 09:06:13 +01:00
d41020c6b8
Merge pull request #27 from Rob-Hague/partialread
Fix partial reads on compressed data
2025-01-09 17:57:44 +01:00
Robert Hague
8d39156d37 Fix partial reads on compressed data 2025-01-01 13:46:59 +01:00
6ccfc15e63
Merge pull request #26 from mahalex/dev/updates
Some updates
2024-07-24 20:49:56 +02:00
e2c8b7d896 Bump version 2024-07-24 20:07:29 +02:00
920a1d680e Update pipelines 2024-07-24 19:48:32 +02:00
29345ef6ab Switch to XUnit 2024-07-24 19:47:16 +02:00
2189c1c055 Test on .NET 8 2024-07-24 19:47:06 +02:00
c0348f3e29 Improve performance of reading objects 2024-07-24 19:10:47 +02:00
404b982226
Merge pull request #21 from mahalex/issues/19-and-20
* Fix #19 
* Fix #20 
* Upgrade test dependencies
* Build and test with .NET 5
2021-07-10 03:13:51 -07:00
d7c9491551 Version bump 2021-07-10 11:59:49 +02:00
5cd2528b71 Fix #20 2021-07-10 11:58:52 +02:00
fffd539f4f Upgrade references 2021-07-10 11:58:52 +02:00
6d899096e4 Fix #19 2021-07-10 10:01:32 +02:00
b6c387a1e4 Version bump 2020-10-10 12:43:55 +02:00
2ebf772dd8 Fix nullability warnings 2020-10-10 12:40:09 +02:00
0b11ec088e
Merge pull request #16 from mahalex/net472
Target .NET Framework 4.7.2
2020-10-10 03:37:14 -07:00
1e98293e3a Target .NET Framework 4.7.2
Fix: #15
2020-10-10 12:20:12 +02:00
6e6c5b7ec2
Merge pull request #13 from JTOne123/master
[PR] The proj files have been updated to enable SourceLink
2020-10-09 01:15:52 -07:00
Pavlo Datsiuk
9d50f4e892 [COMMIT] The proj files have been updated to enable SourceLink [MatFileHandler.csproj] 2020-06-25 14:58:39 +03:00
17f10b40ae
Merge pull request #10 from mahalex/nullable
Use C# 8.0 and enable nullable reference types
2019-10-05 15:24:42 +02:00
f3a2559237 Target .NET Framework 4.6.1 again 2019-10-05 15:21:44 +02:00
662a2b97fb Update build definition 2019-10-05 15:19:05 +02:00
d8f3ac0911 Nullable annotations 2019-10-05 14:47:05 +02:00
aa9f62c4df Upgrade analyzers 2019-10-05 11:04:03 +02:00
1b465c2857 Rewrite DataExtraction 2019-10-05 10:27:37 +02:00
b842bc7be1 Enable nullable reference types 2019-09-29 20:12:13 +02:00
6f4f40e7a4 Target .NET Core 3.0 in test project 2019-09-29 20:11:49 +02:00
2773f4c898 Un-target net461 2019-09-29 20:11:33 +02:00
6fe9eda50b
Merge pull request #7 from mahalex/fix-unrepresentable-datetime-bug
Fix unrepresentable datetime bug
2019-03-04 22:28:43 +01:00
986d4b157e Update XML documentation 2019-03-04 22:18:52 +01:00
4999590eac Replace license url with license file 2019-03-04 22:16:51 +01:00
51ec4c00bb Add license file to the solution 2019-03-04 22:11:09 +01:00
ada4005cab Version bump 2019-03-04 22:09:14 +01:00
2fbd45d643 Handle unrepresentable datetimes 2019-03-04 22:04:06 +01:00
2c514c62f1
Merge pull request #6 from mahalex/fix-objects-parsing
Fixed parsing of MATLAB objects.
Added adapters for string, datetime, and duration arrays.
2019-03-03 16:58:34 +01:00
9ecad85c27 Version bump 2019-03-03 16:53:37 +01:00
de253bf9c0 Fix typos in XML documentation 2019-03-03 16:46:04 +01:00
8fa9d12470 Implement DurationAdapter 2019-03-03 16:45:45 +01:00
78f1033836 Code cleanup 2019-03-03 16:23:55 +01:00
69599c12b5 Implement strings 2019-03-03 16:18:36 +01:00
262c3a8314 Fix parsing of subsystem data 2019-03-02 21:15:44 +01:00
5e70d7fdfd Implement DatetimeAdapter 2019-03-02 12:24:15 +01:00
6a6d0a342a Add description of mxOPAQUE_CLASS objects 2018-10-21 22:26:32 +02:00
092ce8a176 Version bump 2018-10-17 19:16:38 +02:00
793cc8ddb6 Fix parsing of nested objects. 2018-10-17 19:16:06 +02:00
fc3af2a3cf Version bump 2018-10-14 19:58:14 +02:00
0add1d0e6e Update README 2018-10-14 19:57:44 +02:00
428b95b3fb Add XML documentation 2018-10-14 19:13:42 +02:00
64ec29196b
Azure pipelines: tests and pack 2018-10-14 18:06:16 +02:00
4b90063090
Merge pull request #4 from mahalex/dev/opaque-objects
Support for Matlab objects & tables
2018-10-14 14:55:01 +02:00
1018655f8c Add support for tables 2018-10-14 14:52:52 +02:00
10aa558152 Preliminary support for Matlab objects 2018-10-14 14:34:05 +02:00
126d8a7483 Update copyright text 2018-10-14 13:57:57 +02:00
8b3ac5d5d8
Update Pipelines to use Windows vmimage 2018-10-14 13:48:35 +02:00
1017fa5b96 Set up CI with Azure Pipelines 2018-10-14 13:27:24 +02:00
7130ad7476 Version bump 2018-09-20 18:43:34 +02:00
e72c40ca22 Update README 2018-09-20 18:42:22 +02:00
bc0eec02fe Add README.md to solution 2018-09-20 18:36:12 +02:00
5116f06c08 Add options to control compression when writing 2018-09-20 18:32:08 +02:00
89 changed files with 5828 additions and 1379 deletions

248
.editorconfig Normal file
View File

@ -0,0 +1,248 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
dotnet_analyzer_diagnostic.category-Design.severity = warning
dotnet_analyzer_diagnostic.category-Documentation.severity = warning
dotnet_analyzer_diagnostic.category-Globalization.severity = warning
dotnet_analyzer_diagnostic.category-Interoperability.severity = warning
dotnet_analyzer_diagnostic.category-Maintainability.severity = warning
dotnet_analyzer_diagnostic.category-Naming.severity = warning
dotnet_analyzer_diagnostic.category-Performance.severity = warning
dotnet_analyzer_diagnostic.category-Reliability.severity = warning
dotnet_analyzer_diagnostic.category-Security.severity = warning
dotnet_analyzer_diagnostic.category-Style.severity = warning
dotnet_analyzer_diagnostic.category-Usage.severity = warning
dotnet_diagnostic.IDE0010.severity = none
dotnet_diagnostic.IDE0072.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA1861.severity = none
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = false
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = false
dotnet_style_prefer_conditional_expression_over_return = false
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all:silent
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = true:none
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_style_namespace_declarations = file_scoped:none
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = false
csharp_style_prefer_top_level_statements = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright 2017 Alexander Luzgarev
Copyright 2017-2025 Alexander Luzgarev
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,68 +0,0 @@
// Copyright 2017 Alexander Luzgarev
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MatFileHandler.Tests
{
/// <summary>
/// Abstract factory of test data.
/// </summary>
/// <typeparam name="TTestData">Type of test data.</typeparam>
public abstract class AbstractTestDataFactory<TTestData>
{
/// <summary>
/// Initializes a new instance of the <see cref="AbstractTestDataFactory{TTestData}"/> class.
/// </summary>
/// <param name="dataDirectory">Directory with test files.</param>
/// <param name="testFilenameConvention">A convention used to filter test files.</param>
protected AbstractTestDataFactory(string dataDirectory, ITestFilenameConvention testFilenameConvention)
{
DataDirectory = dataDirectory;
TestFilenameConvention = testFilenameConvention;
}
private string DataDirectory { get; }
private ITestFilenameConvention TestFilenameConvention { get; }
/// <summary>
/// Get test data set by name.
/// </summary>
/// <param name="dataSet">Name of the data set.</param>
/// <returns>Test data.</returns>
public TTestData this[string dataSet] =>
ReadTestData(FixPath(TestFilenameConvention.ConvertTestNameToFilename(dataSet)));
/// <summary>
/// Get a sequence of all test data sets in the factory.
/// </summary>
/// <returns>A sequence of data sets.</returns>
public IEnumerable<TTestData> GetAllTestData()
{
var files = Directory.EnumerateFiles(DataDirectory).Where(TestFilenameConvention.FilterFile);
foreach (var filename in files)
{
yield return ReadTestData(filename);
}
}
/// <summary>
/// Read test data from a stream.
/// </summary>
/// <param name="stream">Input stream.</param>
/// <returns>Test data.</returns>
protected abstract TTestData ReadDataFromStream(Stream stream);
private string FixPath(string filename) => Path.Combine(DataDirectory, filename);
private TTestData ReadTestData(string filename)
{
using (var stream = new FileStream(filename, FileMode.Open))
{
return ReadDataFromStream(stream);
}
}
}
}

View File

@ -1,32 +1,29 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.Numerics;
using NUnit.Framework;
using Xunit;
namespace MatFileHandler.Tests
{
/// <summary>
/// Tests of Matlab array manipulation.
/// </summary>
[TestFixture]
public class ArrayHandlingTests
public sealed class ArrayHandlingTests : IDisposable
{
private DataBuilder Builder { get; set; }
/// <summary>
/// Set up a DataBuilder.
/// Setup for array handling tests.
/// </summary>
[SetUp]
public void Setup()
public ArrayHandlingTests()
{
Builder = new DataBuilder();
}
private DataBuilder Builder { get; set; }
/// <summary>
/// Test numerical array creation.
/// </summary>
[Test]
[Fact]
public void TestCreate()
{
TestCreateArrayOf<sbyte>();
@ -54,70 +51,78 @@ namespace MatFileHandler.Tests
/// <summary>
/// Test numerical array manipulation.
/// </summary>
[Test]
[Fact]
public void TestNumArray()
{
var array = Builder.NewArray<int>(2, 3);
array[0, 1] = 2;
Assert.That(array[0, 1], Is.EqualTo(2));
Assert.Equal(2, array[0, 1]);
}
/// <summary>
/// Test cell array manipulation.
/// </summary>
[Test]
[Fact]
public void TestCellArray()
{
var array = Builder.NewCellArray(2, 3);
Assert.That(array.Dimensions, Is.EqualTo(new[] { 2, 3 }));
Assert.Equal(new[] { 2, 3 }, array.Dimensions);
array[0, 1] = Builder.NewArray<int>(1, 2);
Assert.That(array[1, 2].IsEmpty, Is.True);
Assert.That(array[0, 1].IsEmpty, Is.False);
Assert.True(array[1, 2].IsEmpty);
Assert.False(array[0, 1].IsEmpty);
var assigned = (IArrayOf<int>)array[0, 1];
Assert.That(assigned, Is.Not.Null);
Assert.That(assigned.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.NotNull(assigned);
Assert.Equal(new[] { 1, 2 }, assigned.Dimensions);
}
/// <summary>
/// Test structure array manipulation.
/// </summary>
[Test]
[Fact]
public void TestStructureArray()
{
var array = Builder.NewStructureArray(new[] { "x", "y" }, 2, 3);
array["x", 0, 1] = Builder.NewArray<int>(1, 2);
Assert.That(array["y", 0, 1].IsEmpty, Is.True);
Assert.That(array["x", 0, 1].IsEmpty, Is.False);
Assert.True(array["y", 0, 1].IsEmpty);
Assert.False(array["x", 0, 1].IsEmpty);
var assigned = (IArrayOf<int>)array["x", 0, 1];
Assert.That(assigned, Is.Not.Null);
Assert.That(assigned.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.NotNull(assigned);
Assert.Equal(new[] { 1, 2 }, assigned.Dimensions);
}
/// <summary>
/// Test character array manipulation.
/// </summary>
[Test]
[Fact]
public void TestString()
{
var array = Builder.NewCharArray("🍆");
Assert.That(array.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.Equal(new[] { 1, 2 }, array.Dimensions);
}
/// <summary>
/// Test file creation.
/// </summary>
[Test]
[Fact]
public void TestFile()
{
var file = Builder.NewFile(new List<IVariable>());
Assert.That(file, Is.Not.Null);
Assert.NotNull(file);
}
private static void TestCreateArrayOf<T>()
where T : struct
{
var array = new DataBuilder().NewArray<T>(2, 3);
Assert.That(array, Is.Not.Null);
Assert.NotNull(array);
}
/// <summary>
/// Cleanup.
/// </summary>
public void Dispose()
{
}
}
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Xunit;
namespace MatFileHandler.Tests
{
/// <summary>
/// Tests for the <see cref="ChecksumCalculatingStream"/> class.
/// </summary>
public class ChecksumCalculatingStreamTests
{
/// <summary>
/// Test writing various things.
/// </summary>
/// <param name="bytes">Bytes to write.</param>
[Theory]
[MemberData(nameof(TestData))]
public void Test(byte[] bytes)
{
using var stream = new MemoryStream();
var sut = new ChecksumCalculatingStream(stream);
sut.Write(bytes, 0, bytes.Length);
var actual = sut.GetCrc();
var expected = ReferenceCalculation(bytes);
}
/// <summary>
/// Test data for <see cref="Test"/>.
/// </summary>
/// <returns>Test data.</returns>
public static TheoryData<byte[]> TestData()
{
var empty = new byte[1234];
var nonEmpty = new byte[12345];
for (var i = 0; i < 1234; i++)
{
nonEmpty[i] = (byte)((i * i) % 256);
}
return new TheoryData<byte[]>()
{
new byte[] { 0x00 },
new byte[] { 0x01 },
new byte[] { 0xff },
new byte[] { 0xff, 0xff },
new byte[] { 0xff, 0xff, 0xff },
new byte[] { 0xff, 0xff, 0xff, 0xff },
new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff },
new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff },
new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff },
new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff },
new byte[] { 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff },
new byte[] { 0x02, 0x03, 0x05, 0x07, 0x0b, 0x0d, 0x11, 0x13, 0x17, 0x1d },
empty,
nonEmpty,
};
}
private static Action<Stream> BinaryWriterAction(Action<BinaryWriter> action)
{
return stream =>
{
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
action(writer);
};
}
private static uint ReferenceCalculation(byte[] bytes)
{
using var stream = new MemoryStream();
stream.Write(bytes, 0, bytes.Length);
stream.Position = 0;
return CalculateAdler32Checksum(stream);
}
private static uint CalculateAdler32Checksum(MemoryStream stream)
{
uint s1 = 1;
uint s2 = 0;
const uint bigPrime = 0xFFF1;
const int bufferSize = 2048;
var buffer = new byte[bufferSize];
while (true)
{
var bytesRead = stream.Read(buffer, 0, bufferSize);
for (var i = 0; i < bytesRead; i++)
{
s1 = (s1 + buffer[i]) % bigPrime;
s2 = (s2 + s1) % bigPrime;
}
if (bytesRead < bufferSize)
{
break;
}
}
return (s2 << 16) | s1;
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler.Tests
{
/// <summary>
@ -47,4 +45,4 @@ namespace MatFileHandler.Tests
/// </summary>
public static readonly ulong[] UInt64Limits = { 0UL, 18446744073709551615UL };
}
}
}

View File

@ -1,51 +1,48 @@
// Copyright 2017 Alexander Luzgarev
using NUnit.Framework;
using Xunit;
namespace MatFileHandler.Tests
{
/// <summary>
/// Tests of the ComplexOf value type.
/// </summary>
[TestFixture]
public class ComplexOfTests
{
/// <summary>
/// Test getting real and imaginary parts.
/// </summary>
[Test]
[Fact]
public void TestAccessors()
{
var c = new ComplexOf<byte>(1, 2);
Assert.That(c.Real, Is.EqualTo(1));
Assert.That(c.Imaginary, Is.EqualTo(2));
Assert.Equal(1, c.Real);
Assert.Equal(2, c.Imaginary);
}
/// <summary>
/// Test equality operators.
/// </summary>
[Test]
[Fact]
public void TestEquals()
{
var c1 = new ComplexOf<byte>(1, 2);
var c2 = new ComplexOf<byte>(3, 4);
var c3 = new ComplexOf<byte>(1, 2);
Assert.That(c1 == c3, Is.True);
Assert.That(c1 != c2, Is.True);
Assert.That(c2 != c3, Is.True);
Assert.True(c1 == c3);
Assert.True(c1 != c2);
Assert.True(c2 != c3);
}
/// <summary>
/// Test hash code calculation.
/// </summary>
[Test]
[Fact]
public void TestGetHashCode()
{
var c1 = new ComplexOf<byte>(1, 2);
var c2 = new ComplexOf<byte>(1, 2);
var h1 = c1.GetHashCode();
var h2 = c2.GetHashCode();
Assert.That(h1, Is.EqualTo(h2));
Assert.Equal(h1, h2);
}
}
}
}

View File

@ -1,43 +0,0 @@
// Copyright 2017 Alexander Luzgarev
using System.IO;
namespace MatFileHandler.Tests
{
/// <summary>
/// A filename convention based on file extensions.
/// </summary>
internal class ExtensionTestFilenameConvention : ITestFilenameConvention
{
/// <summary>
/// Initializes a new instance of the <see cref="ExtensionTestFilenameConvention"/> class.
/// </summary>
/// <param name="extension">File extension.</param>
public ExtensionTestFilenameConvention(string extension)
{
Extension = extension;
}
private string Extension { get; }
/// <summary>
/// Convert test name to filename by adding the extension.
/// </summary>
/// <param name="testName">Test name.</param>
/// <returns>The corresponding filename.</returns>
public string ConvertTestNameToFilename(string testName)
{
return Path.ChangeExtension(testName, Extension);
}
/// <summary>
/// Compare file's extension to the one specified during initialization.
/// </summary>
/// <param name="filename">Filename.</param>
/// <returns>True iff the file has the extension stored in the class.</returns>
public bool FilterFile(string filename)
{
return Path.GetExtension(filename) == "." + Extension;
}
}
}

View File

@ -1,24 +0,0 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler.Tests
{
/// <summary>
/// Interface for handling filtering tests based on filenames.
/// </summary>
public interface ITestFilenameConvention
{
/// <summary>
/// Convert test name to a filename (e.g., by adding an appropriate extension).
/// </summary>
/// <param name="testName">Name of a test.</param>
/// <returns>Filename.</returns>
string ConvertTestNameToFilename(string testName);
/// <summary>
/// Decide if a file contains a test based on the filename.
/// </summary>
/// <param name="filename">A filename.</param>
/// <returns>True iff the file should contain a test.</returns>
bool FilterFile(string filename);
}
}

View File

@ -1,22 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFrameworks>net461;net472;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<CodeAnalysisRuleSet>..\MatFileHandler.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<CodeAnalysisRuleSet>..\MatFileHandler.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\Debug\netcoreapp2.0\MatFileHandler.Tests.xml</DocumentationFile>
<PropertyGroup>
<DocumentationFile>bin\Debug\net5.0\MatFileHandler.Tests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="NUnit" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Condition=" '$(TargetFramework)' != 'net461' " Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Condition=" '$(TargetFramework)' == 'net461' " Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MatFileHandler\MatFileHandler.csproj" />

View File

@ -1,16 +1,15 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using NUnit.Framework;
using Xunit;
namespace MatFileHandler.Tests
{
/// <summary>
/// Tests of file reading API.
/// </summary>
[TestFixture]
public class MatFileReaderTests
{
private const string TestDirectory = "test-data";
@ -18,27 +17,26 @@ namespace MatFileHandler.Tests
/// <summary>
/// Test reading all files in a given test set.
/// </summary>
/// <param name="testSet">Name of the set.</param>
[TestCase("good")]
public void TestReader(string testSet)
[Theory, MemberData(nameof(TestDataFactories))]
public void TestReader(MatFileReadingMethod method)
{
foreach (var matFile in GetTests(testSet).GetAllTestData())
foreach (var matFile in ReadAllTestFiles(method))
{
Assert.That(matFile.Variables, Is.Not.Empty);
Assert.NotEmpty(matFile.Variables);
}
}
/// <summary>
/// Test reading lower and upper limits of integer data types.
/// </summary>
[Test]
public void TestLimits()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestLimits(MatFileReadingMethod method)
{
var matFile = GetTests("good")["limits"];
var matFile = ReadTestFile("limits", method);
IArray array;
array = matFile["int8_"].Value;
CheckLimits(array as IArrayOf<sbyte>, CommonData.Int8Limits);
Assert.That(array.ConvertToDoubleArray(), Is.EqualTo(new[] { -128.0, 127.0 }));
Assert.Equal(new[] { -128.0, 127.0 }, array.ConvertToDoubleArray());
array = matFile["uint8_"].Value;
CheckLimits(array as IArrayOf<byte>, CommonData.UInt8Limits);
@ -65,16 +63,16 @@ namespace MatFileHandler.Tests
/// <summary>
/// Test writing lower and upper limits of integer-based complex data types.
/// </summary>
[Test]
public void TestComplexLimits()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestComplexLimits(MatFileReadingMethod method)
{
var matFile = GetTests("good")["limits_complex"];
var matFile = ReadTestFile("limits_complex", method);
IArray array;
array = matFile["int8_complex"].Value;
CheckComplexLimits(array as IArrayOf<ComplexOf<sbyte>>, CommonData.Int8Limits);
Assert.That(
array.ConvertToComplexArray(),
Is.EqualTo(new[] { -128.0 + (127.0 * Complex.ImaginaryOne), 127.0 - (128.0 * Complex.ImaginaryOne) }));
Assert.Equal(
new[] { -128.0 + (127.0 * Complex.ImaginaryOne), 127.0 - (128.0 * Complex.ImaginaryOne) },
array.ConvertToComplexArray());
array = matFile["uint8_complex"].Value;
CheckComplexLimits(array as IArrayOf<ComplexOf<byte>>, CommonData.UInt8Limits);
@ -101,218 +99,552 @@ namespace MatFileHandler.Tests
/// <summary>
/// Test reading an ASCII-encoded string.
/// </summary>
[Test]
public void TestAscii()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestAscii(MatFileReadingMethod method)
{
var matFile = GetTests("good")["ascii"];
var matFile = ReadTestFile("ascii", method);
var arrayAscii = matFile["s"].Value as ICharArray;
Assert.That(arrayAscii, Is.Not.Null);
Assert.That(arrayAscii.Dimensions, Is.EqualTo(new[] { 1, 3 }));
Assert.That(arrayAscii.String, Is.EqualTo("abc"));
Assert.That(arrayAscii[2], Is.EqualTo('c'));
Assert.NotNull(arrayAscii);
Assert.Equal(new[] { 1, 3 }, arrayAscii.Dimensions);
Assert.Equal("abc", arrayAscii.String);
Assert.Equal('c', arrayAscii[2]);
}
/// <summary>
/// Test reading a Unicode string.
/// </summary>
[Test]
public void TestUnicode()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestUnicode(MatFileReadingMethod method)
{
var matFile = GetTests("good")["unicode"];
var matFile = ReadTestFile("unicode", method);
var arrayUnicode = matFile["s"].Value as ICharArray;
Assert.That(arrayUnicode, Is.Not.Null);
Assert.That(arrayUnicode.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.That(arrayUnicode.String, Is.EqualTo("必フ"));
Assert.That(arrayUnicode[0], Is.EqualTo('必'));
Assert.That(arrayUnicode[1], Is.EqualTo('フ'));
Assert.NotNull(arrayUnicode);
Assert.Equal(new[] { 1, 2 }, arrayUnicode.Dimensions);
Assert.Equal("必フ", arrayUnicode.String);
Assert.Equal('必', arrayUnicode[0]);
Assert.Equal('フ', arrayUnicode[1]);
}
/// <summary>
/// Test reading a wide Unicode string.
/// </summary>
[Test]
public void TestUnicodeWide()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestUnicodeWide(MatFileReadingMethod method)
{
var matFile = GetTests("good")["unicode-wide"];
var matFile = ReadTestFile("unicode-wide", method);
var arrayUnicodeWide = matFile["s"].Value as ICharArray;
Assert.That(arrayUnicodeWide, Is.Not.Null);
Assert.That(arrayUnicodeWide.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.That(arrayUnicodeWide.String, Is.EqualTo("🍆"));
Assert.NotNull(arrayUnicodeWide);
Assert.Equal(new[] { 1, 2 }, arrayUnicodeWide.Dimensions);
Assert.Equal("🍆", arrayUnicodeWide.String);
}
/// <summary>
/// Test converting a structure array to a Double array.
/// </summary>
/// <returns>Should return null.</returns>
[Test(ExpectedResult = null)]
public double[] TestConvertToDoubleArray()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestConvertToDoubleArray(MatFileReadingMethod method)
{
var matFile = GetTests("good")["struct"];
var matFile = ReadTestFile("struct", method);
var array = matFile.Variables[0].Value;
return array.ConvertToDoubleArray();
Assert.Null(array.ConvertToDoubleArray());
}
/// <summary>
/// Test converting a structure array to a Complex array.
/// </summary>
/// <returns>Should return null.</returns>
[Test(ExpectedResult = null)]
public Complex[] TestConvertToComplexArray()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestConvertToComplexArray(MatFileReadingMethod method)
{
var matFile = GetTests("good")["struct"];
var matFile = ReadTestFile("struct", method);
var array = matFile.Variables[0].Value;
return array.ConvertToComplexArray();
Assert.Null(array.ConvertToComplexArray());
}
/// <summary>
/// Test reading an enumeration.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestEnum(MatFileReadingMethod method)
{
var matFile = ReadTestFile("enum", method);
var days = matFile["days"].Value;
var enumeration = new EnumAdapter(days);
Assert.Equal(5, enumeration.Values.Count);
Assert.Equal("Wednesday", enumeration.ValueNames[enumeration.Values[0]]);
Assert.Equal("Saturday", enumeration.ValueNames[enumeration.Values[1]]);
Assert.Equal("Monday", enumeration.ValueNames[enumeration.Values[2]]);
Assert.Equal("Wednesday", enumeration.ValueNames[enumeration.Values[3]]);
Assert.Equal("Saturday", enumeration.ValueNames[enumeration.Values[4]]);
}
/// <summary>
/// Test reading a structure array.
/// </summary>
[Test]
public void TestStruct()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestStruct(MatFileReadingMethod method)
{
var matFile = GetTests("good")["struct"];
var matFile = ReadTestFile("struct", method);
var structure = matFile["struct_"].Value as IStructureArray;
Assert.That(structure, Is.Not.Null);
Assert.That(structure.FieldNames, Is.EquivalentTo(new[] { "x", "y" }));
Assert.NotNull(structure);
Assert.Equal(new[] { "x", "y" }, structure.FieldNames);
var element = structure[0, 0];
Assert.That(element.ContainsKey("x"), Is.True);
Assert.That(element.Count, Is.EqualTo(2));
Assert.That(element.TryGetValue("x", out var _), Is.True);
Assert.That(element.TryGetValue("z", out var _), Is.False);
Assert.That(element.Keys, Has.Exactly(2).Items);
Assert.That(element.Values, Has.Exactly(2).Items);
Assert.True(element.ContainsKey("x"));
Assert.Equal(2, element.Count);
Assert.True(element.TryGetValue("x", out var _));
Assert.False(element.TryGetValue("z", out var _));
Assert.Equal(2, element.Keys.Count());
Assert.Equal(2, element.Values.Count());
var keys = element.Select(pair => pair.Key);
Assert.That(keys, Is.EquivalentTo(new[] { "x", "y" }));
Assert.Equal(new[] { "x", "y" }, keys);
Assert.That((element["x"] as IArrayOf<double>)?[0], Is.EqualTo(12.345));
Assert.Equal(12.345, (element["x"] as IArrayOf<double>)?[0]);
Assert.That((structure["x", 0, 0] as IArrayOf<double>)?[0], Is.EqualTo(12.345));
Assert.That((structure["y", 0, 0] as ICharArray)?.String, Is.EqualTo("abc"));
Assert.That((structure["x", 1, 0] as ICharArray)?.String, Is.EqualTo("xyz"));
Assert.That(structure["y", 1, 0].IsEmpty, Is.True);
Assert.That((structure["x", 0, 1] as IArrayOf<double>)?[0], Is.EqualTo(2.0));
Assert.That((structure["y", 0, 1] as IArrayOf<double>)?[0], Is.EqualTo(13.0));
Assert.That(structure["x", 1, 1].IsEmpty, Is.True);
Assert.That((structure["y", 1, 1] as ICharArray)?[0, 0], Is.EqualTo('a'));
Assert.That(((structure["x", 0, 2] as ICellArray)?[0] as ICharArray)?.String, Is.EqualTo("x"));
Assert.That(((structure["x", 0, 2] as ICellArray)?[1] as ICharArray)?.String, Is.EqualTo("yz"));
Assert.That((structure["y", 0, 2] as IArrayOf<double>)?.Dimensions, Is.EqualTo(new[] { 2, 3 }));
Assert.That((structure["y", 0, 2] as IArrayOf<double>)?[0, 2], Is.EqualTo(3.0));
Assert.That((structure["x", 1, 2] as IArrayOf<float>)?[0], Is.EqualTo(1.5f));
Assert.That(structure["y", 1, 2].IsEmpty, Is.True);
Assert.Equal(12.345, (structure["x", 0, 0] as IArrayOf<double>)?[0]);
Assert.Equal(2.0, (structure["x", 0, 1] as IArrayOf<double>)?[0]);
Assert.Equal("x", ((structure["x", 0, 2] as ICellArray)?[0] as ICharArray)?.String);
Assert.Equal("yz", ((structure["x", 0, 2] as ICellArray)?[1] as ICharArray)?.String);
Assert.Equal("xyz", (structure["x", 1, 0] as ICharArray)?.String);
Assert.True(structure["x", 1, 1].IsEmpty);
Assert.Equal(1.5f, (structure["x", 1, 2] as IArrayOf<float>)?[0]);
Assert.Equal("abc", (structure["y", 0, 0] as ICharArray)?.String);
Assert.Equal(13.0, (structure["y", 0, 1] as IArrayOf<double>)?[0]);
Assert.Equal(new[] { 2, 3 }, (structure["y", 0, 2] as IArrayOf<double>)?.Dimensions);
Assert.Equal(3.0, (structure["y", 0, 2] as IArrayOf<double>)?[0, 2]);
Assert.True(structure["y", 1, 0].IsEmpty);
Assert.Equal('a', (structure["y", 1, 1] as ICharArray)?[0, 0]);
Assert.True(structure["y", 1, 2].IsEmpty);
Assert.Equal(
new double[,]
{
{ 1, 2, 3 },
{ 4, 5, 6 },
},
structure["y", 0, 2].ConvertTo2dDoubleArray());
Assert.Equal(
new double[,]
{
{ 1, 2, 3 },
{ 4, 5, 6 },
},
structure["y", 0, 2].ConvertToMultidimensionalDoubleArray());
}
/// <summary>
/// Test reading a sparse array.
/// </summary>
[Test]
public void TestSparse()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestSparse(MatFileReadingMethod method)
{
var matFile = GetTests("good")["sparse"];
var matFile = ReadTestFile("sparse", method);
var sparseArray = matFile["sparse_"].Value as ISparseArrayOf<double>;
Assert.That(sparseArray, Is.Not.Null);
Assert.That(sparseArray.Dimensions, Is.EqualTo(new[] { 4, 5 }));
Assert.That(sparseArray.Data[(1, 1)], Is.EqualTo(1.0));
Assert.That(sparseArray[1, 1], Is.EqualTo(1.0));
Assert.That(sparseArray[1, 2], Is.EqualTo(2.0));
Assert.That(sparseArray[2, 1], Is.EqualTo(3.0));
Assert.That(sparseArray[2, 3], Is.EqualTo(4.0));
Assert.That(sparseArray[0, 4], Is.EqualTo(0.0));
Assert.That(sparseArray[3, 0], Is.EqualTo(0.0));
Assert.That(sparseArray[3, 4], Is.EqualTo(0.0));
Assert.NotNull(sparseArray);
Assert.Equal(new[] { 4, 5 }, sparseArray.Dimensions);
Assert.Equal(1.0, sparseArray.Data[(1, 1)]);
Assert.Equal(1.0, sparseArray[1, 1]);
Assert.Equal(2.0, sparseArray[1, 2]);
Assert.Equal(3.0, sparseArray[2, 1]);
Assert.Equal(4.0, sparseArray[2, 3]);
Assert.Equal(0.0, sparseArray[0, 4]);
Assert.Equal(0.0, sparseArray[3, 0]);
Assert.Equal(0.0, sparseArray[3, 4]);
Assert.Equal(
new double[,]
{
{ 0, 0, 0, 0, 0 },
{ 0, 1, 2, 0, 0 },
{ 0, 3, 0, 4, 0 },
{ 0, 0, 0, 0, 0 },
},
sparseArray.ConvertTo2dDoubleArray());
}
/// <summary>
/// Test reading a logical array.
/// </summary>
[Test]
public void TestLogical()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestLogical(MatFileReadingMethod method)
{
var matFile = GetTests("good")["logical"];
var matFile = ReadTestFile("logical", method);
var array = matFile["logical_"].Value;
var logicalArray = array as IArrayOf<bool>;
Assert.That(logicalArray, Is.Not.Null);
Assert.That(logicalArray[0, 0], Is.True);
Assert.That(logicalArray[0, 1], Is.True);
Assert.That(logicalArray[0, 2], Is.False);
Assert.That(logicalArray[1, 0], Is.False);
Assert.That(logicalArray[1, 1], Is.True);
Assert.That(logicalArray[1, 2], Is.True);
Assert.NotNull(logicalArray);
Assert.True(logicalArray[0, 0]);
Assert.True(logicalArray[0, 1]);
Assert.False(logicalArray[0, 2]);
Assert.False(logicalArray[1, 0]);
Assert.True(logicalArray[1, 1]);
Assert.True(logicalArray[1, 2]);
}
/// <summary>
/// Test reading a sparse logical array.
/// </summary>
[Test]
public void TestSparseLogical()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestSparseLogical(MatFileReadingMethod method)
{
var matFile = GetTests("good")["sparse_logical"];
var matFile = ReadTestFile("sparse_logical", method);
var array = matFile["sparse_logical"].Value;
var sparseArray = array as ISparseArrayOf<bool>;
Assert.That(sparseArray, Is.Not.Null);
Assert.That(sparseArray.Data[(0, 0)], Is.True);
Assert.That(sparseArray[0, 0], Is.True);
Assert.That(sparseArray[0, 1], Is.True);
Assert.That(sparseArray[0, 2], Is.False);
Assert.That(sparseArray[1, 0], Is.False);
Assert.That(sparseArray[1, 1], Is.True);
Assert.That(sparseArray[1, 2], Is.True);
Assert.NotNull (sparseArray);
Assert.True(sparseArray.Data[(0, 0)]);
Assert.True(sparseArray[0, 0]);
Assert.True(sparseArray[0, 1]);
Assert.False(sparseArray[0, 2]);
Assert.False(sparseArray[1, 0]);
Assert.True(sparseArray[1, 1]);
Assert.True(sparseArray[1, 2]);
}
/// <summary>
/// Test reading a global variable.
/// </summary>
[Test]
public void TestGlobal()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestGlobal(MatFileReadingMethod method)
{
var matFile = GetTests("good")["global"];
var matFile = ReadTestFile("global", method);
var variable = matFile.Variables.First();
Assert.That(variable.IsGlobal, Is.True);
Assert.True(variable.IsGlobal);
}
/// <summary>
/// Test reading a sparse complex array.
/// </summary>
[Test]
public void TextSparseComplex()
[Theory, MemberData(nameof(TestDataFactories))]
public void TextSparseComplex(MatFileReadingMethod method)
{
var matFile = GetTests("good")["sparse_complex"];
var matFile = ReadTestFile("sparse_complex", method);
var array = matFile["sparse_complex"].Value;
var sparseArray = array as ISparseArrayOf<Complex>;
Assert.That(sparseArray, Is.Not.Null);
Assert.That(sparseArray[0, 0], Is.EqualTo(-1.5 + (2.5 * Complex.ImaginaryOne)));
Assert.That(sparseArray[1, 0], Is.EqualTo(2 - (3 * Complex.ImaginaryOne)));
Assert.That(sparseArray[0, 1], Is.EqualTo(Complex.Zero));
Assert.That(sparseArray[1, 1], Is.EqualTo(0.5 + (1.0 * Complex.ImaginaryOne)));
Assert.NotNull(sparseArray);
Assert.Equal(-1.5 + (2.5 * Complex.ImaginaryOne), sparseArray[0, 0]);
Assert.Equal(2 - (3 * Complex.ImaginaryOne), sparseArray[1, 0]);
Assert.Equal(Complex.Zero, sparseArray[0, 1]);
Assert.Equal(0.5 + (1.0 * Complex.ImaginaryOne), sparseArray[1, 1]);
}
/// <summary>
/// Test reading an object.
/// </summary>
[Test]
public void TestObject()
[Theory, MemberData(nameof(TestDataFactories))]
public void TestObject(MatFileReadingMethod method)
{
Assert.That(() => GetTests("bad")["object"], Throws.TypeOf<HandlerException>());
var matFile = ReadTestFile("object", method);
var obj = matFile["object_"].Value as IMatObject;
Assert.NotNull(obj);
Assert.Equal("Point", obj.ClassName);
Assert.Equal(new[] { "x", "y" }, obj.FieldNames);
Assert.Equal(new[] { 3.0 }, obj["x", 0].ConvertToDoubleArray());
Assert.Equal(new[] { 5.0 }, obj["y", 0].ConvertToDoubleArray());
Assert.Equal(new[] { -2.0 }, obj["x", 1].ConvertToDoubleArray());
Assert.Equal(new[] { 6.0 }, obj["y", 1].ConvertToDoubleArray());
}
private static AbstractTestDataFactory<IMatFile> GetTests(string factoryName) =>
new MatTestDataFactory(Path.Combine(TestDirectory, factoryName));
/// <summary>
/// Test reading another object.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestObject2(MatFileReadingMethod method)
{
var matFile = ReadTestFile("object2", method);
var obj = matFile["object2"].Value as IMatObject;
Assert.NotNull(obj);
Assert.Equal("Point", obj.ClassName);
Assert.Equal(new[] { "x", "y" }, obj.FieldNames);
Assert.Equal(new[] { 3.0 }, obj["x", 0, 0].ConvertToDoubleArray());
Assert.Equal(new[] { -2.0 }, obj["x", 0, 1].ConvertToDoubleArray());
Assert.Equal(new[] { 1.0 }, obj["x", 1, 0].ConvertToDoubleArray());
Assert.Equal(new[] { 0.0 }, obj["x", 1, 1].ConvertToDoubleArray());
Assert.Equal(new[] { 5.0 }, obj["y", 0, 0].ConvertToDoubleArray());
Assert.Equal(new[] { 6.0 }, obj["y", 0, 1].ConvertToDoubleArray());
Assert.Equal(new[] { 0.0 }, obj["y", 1, 0].ConvertToDoubleArray());
Assert.Equal(new[] { 1.0 }, obj["y", 1, 1].ConvertToDoubleArray());
Assert.Equal(new[] { -2.0 }, obj[0, 1]["x"].ConvertToDoubleArray());
Assert.Equal(new[] { -2.0 }, obj[2]["x"].ConvertToDoubleArray());
}
/// <summary>
/// Test reading a table.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestTable(MatFileReadingMethod method)
{
var matFile = ReadTestFile("table", method);
var obj = matFile["table_"].Value as IMatObject;
var table = new TableAdapter(obj);
Assert.Equal(3, table.NumberOfRows);
Assert.Equal(2, table.NumberOfVariables);
Assert.Equal("Some table", table.Description);
Assert.Equal(new[] { "variable1", "variable2" }, table.VariableNames);
var variable1 = table["variable1"] as ICellArray;
Assert.Equal("First row", (variable1[0] as ICharArray).String);
Assert.Equal("Second row", (variable1[1] as ICharArray).String);
Assert.Equal("Third row", (variable1[2] as ICharArray).String);
var variable2 = table["variable2"];
Assert.Equal(new[] { 1.0, 3.0, 5.0, 2.0, 4.0, 6.0 }, variable2.ConvertToDoubleArray());
}
/// <summary>
/// Test reading a deeply nested table.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestDeepTable(MatFileReadingMethod method)
{
var matFile = ReadTestFile("table-deep", method);
var obj = matFile["t"].Value as IMatObject;
var table = new TableAdapter(obj);
Assert.Equal(1, table.NumberOfRows);
Assert.Equal(2, table.NumberOfVariables);
Assert.Equal(new[] { "s", "another" }, table.VariableNames);
var s = table["s"] as IStructureArray;
Assert.Equal(new[] { "a", "b", "c" }, s.FieldNames);
var c = s["c", 0];
var internalTable = new TableAdapter(c);
Assert.Equal(2, internalTable.NumberOfRows);
Assert.Equal(2, internalTable.NumberOfVariables);
Assert.Equal(new[] { "x", "y" }, internalTable.VariableNames);
var y = new StringAdapter(internalTable["y"]);
Assert.Equal("3", y[0]);
Assert.Equal("abc", y[1]);
}
/// <summary>
/// Test reading a table with strings
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestTableWithStrings(MatFileReadingMethod method)
{
var matFile = ReadTestFile("table-with-strings", method);
var obj = matFile["t"].Value as IMatObject;
var table = new TableAdapter(obj);
Assert.Equal(5, table.NumberOfRows);
Assert.Equal(2, table.NumberOfVariables);
Assert.Equal(new[] { "Numbers", "Names" }, table.VariableNames);
var variable = table["Names"] as ICellArray;
var name0 = new StringAdapter(variable[0]);
Assert.Equal("One", name0[0]);
var name1 = new StringAdapter(variable[1]);
Assert.Equal("Two", name1[0]);
var name2 = new StringAdapter(variable[2]);
Assert.Equal("Three", name2[0]);
var name3 = new StringAdapter(variable[3]);
Assert.Equal("Four", name3[0]);
var name4 = new StringAdapter(variable[4]);
Assert.Equal("Five", name4[0]);
}
/// <summary>
/// Test subobjects within objects.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestSubobjects(MatFileReadingMethod method)
{
var matFile = ReadTestFile("pointWithSubpoints", method);
var p = matFile["p"].Value as IMatObject;
Assert.Equal("Point", p.ClassName);
var x = p["x"] as IMatObject;
Assert.Equal("SubPoint", x.ClassName);
Assert.Equal(new[] { "a", "b", "c" }, x.FieldNames);
var y = p["y"] as IMatObject;
Assert.Equal("SubPoint", y.ClassName);
Assert.Equal(new[] { "a", "b", "c" }, y.FieldNames);
Assert.Equal(new[] { 1.0 }, x["a"].ConvertToDoubleArray());
Assert.Equal(new[] { 2.0 }, x["b"].ConvertToDoubleArray());
Assert.Equal(new[] { 3.0 }, x["c"].ConvertToDoubleArray());
Assert.Equal(new[] { 14.0 }, y["a"].ConvertToDoubleArray());
Assert.Equal(new[] { 15.0 }, y["b"].ConvertToDoubleArray());
Assert.Equal(new[] { 16.0 }, y["c"].ConvertToDoubleArray());
}
/// <summary>
/// Test nested objects.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestNestedObjects(MatFileReadingMethod method)
{
var matFile = ReadTestFile("subsubPoint", method);
var p = matFile["p"].Value as IMatObject;
Assert.Equal("Point", p.ClassName);
Assert.Equal(new[] { 1.0 }, p["x"].ConvertToDoubleArray());
var pp = p["y"] as IMatObject;
Assert.True(pp.ClassName == "Point");
Assert.Equal(new[] { 10.0 }, pp["x"].ConvertToDoubleArray());
var ppp = pp["y"] as IMatObject;
Assert.Equal(new[] { 100.0 }, ppp["x"].ConvertToDoubleArray());
Assert.Equal(new[] { 200.0 }, ppp["y"].ConvertToDoubleArray());
}
/// <summary>
/// Test datetime objects.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestDatetime(MatFileReadingMethod method)
{
var matFile = ReadTestFile("datetime", method);
var d = matFile["d"].Value as IMatObject;
var datetime = new DatetimeAdapter(d);
Assert.Equal(new[] { 1, 2 }, datetime.Dimensions);
Assert.Equal(new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero), datetime[0]);
Assert.Equal(new DateTimeOffset(1987, 1, 2, 3, 4, 5, TimeSpan.Zero), datetime[1]);
}
/// <summary>
/// Another test for datetime objects.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestDatetime2(MatFileReadingMethod method)
{
var matFile = ReadTestFile("datetime2", method);
var d = matFile["d"].Value as IMatObject;
var datetime = new DatetimeAdapter(d);
Assert.Equal(new[] { 1, 1 }, datetime.Dimensions);
var diff = new DateTimeOffset(2, 1, 1, 1, 1, 1, 235, TimeSpan.Zero);
Assert.True(datetime[0] - diff < TimeSpan.FromMilliseconds(1));
Assert.True(diff - datetime[0] < TimeSpan.FromMilliseconds(1));
}
/// <summary>
/// Test string objects.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestString(MatFileReadingMethod method)
{
var matFile = ReadTestFile("string", method);
var s = matFile["s"].Value as IMatObject;
var str = new StringAdapter(s);
Assert.Equal(new[] { 4, 1 }, str.Dimensions);
Assert.Equal("abc", str[0]);
Assert.Equal("defgh", str[1]);
Assert.Equal("абвгд", str[2]);
Assert.Equal("æøå", str[3]);
}
/// <summary>
/// Test duration objects.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestDuration(MatFileReadingMethod method)
{
var matFile = ReadTestFile("duration", method);
var d = matFile["d"].Value as IMatObject;
var duration = new DurationAdapter(d);
Assert.Equal(new[] { 1, 3 }, duration.Dimensions);
Assert.Equal(TimeSpan.FromTicks(12345678L), duration[0]);
Assert.Equal(new TimeSpan(0, 2, 4), duration[1]);
Assert.Equal(new TimeSpan(1, 3, 5), duration[2]);
}
/// <summary>
/// Test unrepresentable datetime.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void TestDatetime_Unrepresentable(MatFileReadingMethod method)
{
var matFile = ReadTestFile("datetime-unrepresentable", method);
var obj = matFile["d"].Value as IMatObject;
var datetime = new DatetimeAdapter(obj);
var d0 = datetime[0];
Assert.Null(d0);
}
/// <summary>
/// Test 3-dimensional arrays.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void Test_3DArrays(MatFileReadingMethod method)
{
var matFile = ReadTestFile("issue20", method);
var obj = matFile["a3d"].Value;
var values = obj.ConvertToDoubleArray();
Assert.Equal(Enumerable.Range(1, 24).Select(x => (double)x).ToArray(), values);
var expected = new double[3, 4, 2]
{
{
{ 1, 13 },
{ 4, 16 },
{ 7, 19 },
{ 10, 22 },
},
{
{ 2, 14 },
{ 5, 17 },
{ 8, 20 },
{ 11, 23 },
},
{
{ 3, 15 },
{ 6, 18 },
{ 9, 21 },
{ 12, 24 },
},
};
Assert.Equal(expected, obj.ConvertToMultidimensionalDoubleArray());
Assert.Null(obj.ConvertTo2dDoubleArray());
}
/// <summary>
/// Test four-dimensional arrays.
/// </summary>
[Theory, MemberData(nameof(TestDataFactories))]
public void Test4DArrays(MatFileReadingMethod method)
{
var matFile = ReadTestFile("issue20", method);
var obj = matFile["a4d"].Value;
Assert.Equal(Enumerable.Range(1, 120).Select(x => (double)x).ToArray(), obj.ConvertToDoubleArray());
Assert.Null(obj.ConvertTo2dDoubleArray());
}
/// <summary>
/// Returns the factories that provide test data in various configurations.
/// </summary>
public static TheoryData<MatFileReadingMethod> TestDataFactories
{
get
{
return new TheoryData<MatFileReadingMethod>
{
MatFileReadingMethod.NormalStream,
MatFileReadingMethod.PartialStream,
MatFileReadingMethod.UnalignedStream,
};
}
}
private static IMatFile ReadTestFile(string fileName, MatFileReadingMethod method)
{
var fullFileName = Path.Combine("test-data", "good", $"{fileName}.mat");
return MatFileReadingMethods.ReadMatFile(method, fullFileName);
}
private static IEnumerable<IMatFile> ReadAllTestFiles(MatFileReadingMethod method)
{
foreach (var fileName in Directory.EnumerateFiles(
Path.Combine("test-data", "good"),
"*.mat"))
{
var fullFileName = fileName;
yield return MatFileReadingMethods.ReadMatFile(method, fullFileName);
}
}
private static void CheckLimits<T>(IArrayOf<T> array, T[] limits)
where T : struct
{
Assert.That(array, Is.Not.Null);
Assert.That(array.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.That(array.Data, Is.EqualTo(limits));
Assert.NotNull(array);
Assert.Equal(new[] { 1, 2 }, array.Dimensions);
Assert.Equal(limits, array.Data);
}
private static void CheckComplexLimits<T>(IArrayOf<ComplexOf<T>> array, T[] limits)
where T : struct
{
Assert.That(array, Is.Not.Null);
Assert.That(array.Dimensions, Is.EqualTo(new[] { 1, 2 }));
Assert.That(array[0], Is.EqualTo(new ComplexOf<T>(limits[0], limits[1])));
Assert.That(array[1], Is.EqualTo(new ComplexOf<T>(limits[1], limits[0])));
Assert.NotNull(array);
Assert.Equal(new[] { 1, 2 }, array.Dimensions);
Assert.Equal(new ComplexOf<T>(limits[0], limits[1]), array[0]);
Assert.Equal(new ComplexOf<T>(limits[1], limits[0]), array[1]);
}
}
}
}

View File

@ -0,0 +1,27 @@
namespace MatFileHandler.Tests;
/// <summary>
/// Method of reading .mat files for testing.
/// </summary>
public enum MatFileReadingMethod
{
/// <summary>
/// Undefined.
/// </summary>
Undefined = 0,
/// <summary>
/// Normal stream (like memory or file stream).
/// </summary>
NormalStream,
/// <summary>
/// Partial stream (only is capable of reading one byte at a time).
/// </summary>
PartialStream,
/// <summary>
/// Unaligned stream (what happens if the data don't start at the beginning?).
/// </summary>
UnalignedStream,
}

View File

@ -0,0 +1,38 @@
using System;
using System.IO;
namespace MatFileHandler.Tests;
internal static class MatFileReadingMethods
{
public static IMatFile ReadMatFile(MatFileReadingMethod method, string fullFileName)
{
using var stream = File.OpenRead(fullFileName);
switch (method)
{
case MatFileReadingMethod.NormalStream:
return ReadFromStream(stream);
case MatFileReadingMethod.PartialStream:
{
using var wrapper = new PartialUnseekableReadStream(stream);
return ReadFromStream(wrapper);
}
case MatFileReadingMethod.UnalignedStream:
{
using var ms = new MemoryStream();
ms.Seek(3, SeekOrigin.Begin);
stream.CopyTo(ms);
ms.Seek(3, SeekOrigin.Begin);
return ReadFromStream(ms);
}
default:
throw new NotImplementedException();
}
}
private static IMatFile ReadFromStream(Stream stream)
{
var reader = new MatFileReader(stream);
return reader.Read();
}
}

View File

@ -0,0 +1,27 @@
namespace MatFileHandler.Tests;
/// <summary>
/// Options to give to MatFileWriter constructor for testing it.
/// </summary>
public enum MatFileWriterOptionsForTests
{
/// <summary>
/// Undefined.
/// </summary>
Undefined = 0,
/// <summary>
/// No options.
/// </summary>
None,
/// <summary>
/// Option to always use compression.
/// </summary>
Always,
/// <summary>
/// Option to never use compression.
/// </summary>
Never,
}

View File

@ -1,16 +1,13 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.IO;
using System.Numerics;
using NUnit.Framework;
using Xunit;
namespace MatFileHandler.Tests
{
/// <summary>
/// Tests of file writing API.
/// </summary>
[TestFixture]
public class MatFileWriterTests
{
private const string TestDirectory = "test-data";
@ -18,8 +15,8 @@ namespace MatFileHandler.Tests
/// <summary>
/// Test writing a simple Double array.
/// </summary>
[Test]
public void TestWrite()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestWrite(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var array = builder.NewArray<double>(1, 2);
@ -27,13 +24,13 @@ namespace MatFileHandler.Tests
array[1] = 17.0;
var variable = builder.NewVariable("test", array);
var actual = builder.NewFile(new[] { variable });
MatCompareWithTestData("good", "double-array", actual);
MatCompareWithTestData("good", "double-array", actual, method, options);
}
/// <summary>
/// Test writing a large file.
/// </summary>
[Test]
[Fact]
public void TestHuge()
{
var builder = new DataBuilder();
@ -52,8 +49,8 @@ namespace MatFileHandler.Tests
/// <summary>
/// Test writing lower and upper limits of integer data types.
/// </summary>
[Test]
public void TestLimits()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestLimits(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var int8 = builder.NewVariable("int8_", builder.NewArray(CommonData.Int8Limits, 1, 2));
@ -65,14 +62,14 @@ namespace MatFileHandler.Tests
var int64 = builder.NewVariable("int64_", builder.NewArray(CommonData.Int64Limits, 1, 2));
var uint64 = builder.NewVariable("uint64_", builder.NewArray(CommonData.UInt64Limits, 1, 2));
var actual = builder.NewFile(new[] { int16, int32, int64, int8, uint16, uint32, uint64, uint8 });
MatCompareWithTestData("good", "limits", actual);
MatCompareWithTestData("good", "limits", actual, method, options);
}
/// <summary>
/// Test writing lower and upper limits of integer-based complex data types.
/// </summary>
[Test]
public void TestLimitsComplex()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestLimitsComplex(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var int8Complex = builder.NewVariable(
@ -104,26 +101,26 @@ namespace MatFileHandler.Tests
int16Complex, int32Complex, int64Complex, int8Complex,
uint16Complex, uint32Complex, uint64Complex, uint8Complex,
});
MatCompareWithTestData("good", "limits_complex", actual);
MatCompareWithTestData("good", "limits_complex", actual, method, options);
}
/// <summary>
/// Test writing a wide-Unicode symbol.
/// </summary>
[Test]
public void TestUnicodeWide()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestUnicodeWide(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var s = builder.NewVariable("s", builder.NewCharArray("🍆"));
var actual = builder.NewFile(new[] { s });
MatCompareWithTestData("good", "unicode-wide", actual);
MatCompareWithTestData("good", "unicode-wide", actual, method, options);
}
/// <summary>
/// Test writing a sparse array.
/// </summary>
[Test]
public void TestSparseArray()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestSparseArray(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var sparseArray = builder.NewSparseArray<double>(4, 5);
@ -133,14 +130,14 @@ namespace MatFileHandler.Tests
sparseArray[2, 3] = 4;
var sparse = builder.NewVariable("sparse_", sparseArray);
var actual = builder.NewFile(new[] { sparse });
MatCompareWithTestData("good", "sparse", actual);
MatCompareWithTestData("good", "sparse", actual, method, options);
}
/// <summary>
/// Test writing a structure array.
/// </summary>
[Test]
public void TestStructure()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestStructure(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var structure = builder.NewStructureArray(new[] { "x", "y" }, 2, 3);
@ -161,27 +158,27 @@ namespace MatFileHandler.Tests
structure["y", 1, 2] = builder.NewEmpty();
var struct_ = builder.NewVariable("struct_", structure);
var actual = builder.NewFile(new[] { struct_ });
MatCompareWithTestData("good", "struct", actual);
MatCompareWithTestData("good", "struct", actual, method, options);
}
/// <summary>
/// Test writing a logical array.
/// </summary>
[Test]
public void TestLogical()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestLogical(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var logical = builder.NewArray(new[] { true, false, true, true, false, true }, 2, 3);
var logicalVariable = builder.NewVariable("logical_", logical);
var actual = builder.NewFile(new[] { logicalVariable });
MatCompareWithTestData("good", "logical", actual);
MatCompareWithTestData("good", "logical", actual, method, options);
}
/// <summary>
/// Test writing a sparse logical array.
/// </summary>
[Test]
public void TestSparseLogical()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestSparseLogical(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var array = builder.NewSparseArray<bool>(2, 3);
@ -191,14 +188,14 @@ namespace MatFileHandler.Tests
array[1, 2] = true;
var sparseLogical = builder.NewVariable("sparse_logical", array);
var actual = builder.NewFile(new[] { sparseLogical });
MatCompareWithTestData("good", "sparse_logical", actual);
MatCompareWithTestData("good", "sparse_logical", actual, method, options);
}
/// <summary>
/// Test writing a sparse complex array.
/// </summary>
[Test]
public void TestSparseComplex()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestSparseComplex(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var array = builder.NewSparseArray<Complex>(2, 2);
@ -207,38 +204,60 @@ namespace MatFileHandler.Tests
array[1, 1] = 0.5 + Complex.ImaginaryOne;
var sparseComplex = builder.NewVariable("sparse_complex", array);
var actual = builder.NewFile(new[] { sparseComplex });
MatCompareWithTestData("good", "sparse_complex", actual);
MatCompareWithTestData("good", "sparse_complex", actual, method, options);
}
/// <summary>
/// Test writing a global variable.
/// </summary>
[Test]
public void TestGlobal()
[Theory, MemberData(nameof(MatFileWritingTestData))]
public void TestGlobal(MatFileWritingMethod method, MatFileWriterOptionsForTests options)
{
var builder = new DataBuilder();
var array = builder.NewArray(new double[] { 1, 3, 5 }, 1, 3);
var global = builder.NewVariable("global_", array, true);
var actual = builder.NewFile(new[] { global });
MatCompareWithTestData("good", "global", actual);
MatCompareWithTestData("good", "global", actual, method, options);
}
private static AbstractTestDataFactory<IMatFile> GetMatTestData(string factoryName) =>
new MatTestDataFactory(Path.Combine(TestDirectory, factoryName));
/// <summary>
/// Various writing methods for testing writing of .mat files.
/// </summary>
public static TheoryData<MatFileWritingMethod, MatFileWriterOptionsForTests> MatFileWritingTestData
{
get
{
var always = new MatFileWriterOptions { UseCompression = CompressionUsage.Always};
var never = new MatFileWriterOptions { UseCompression = CompressionUsage.Never };
var data = new TheoryData<MatFileWritingMethod, MatFileWriterOptionsForTests>
{
{ MatFileWritingMethod.NormalStream, MatFileWriterOptionsForTests.None },
{ MatFileWritingMethod.NormalStream, MatFileWriterOptionsForTests.Always },
{ MatFileWritingMethod.NormalStream, MatFileWriterOptionsForTests.Never },
{ MatFileWritingMethod.UnseekableStream, MatFileWriterOptionsForTests.None },
{ MatFileWritingMethod.UnseekableStream, MatFileWriterOptionsForTests.Always },
{ MatFileWritingMethod.UnseekableStream, MatFileWriterOptionsForTests.Never },
{ MatFileWritingMethod.UnalignedStream, MatFileWriterOptionsForTests.None },
{ MatFileWritingMethod.UnalignedStream, MatFileWriterOptionsForTests.Always },
{ MatFileWritingMethod.UnalignedStream, MatFileWriterOptionsForTests.Never },
};
return data;
}
}
private void CompareSparseArrays<T>(ISparseArrayOf<T> expected, ISparseArrayOf<T> actual)
private static void CompareSparseArrays<T>(ISparseArrayOf<T> expected, ISparseArrayOf<T> actual)
where T : struct
{
Assert.That(actual, Is.Not.Null);
Assert.That(expected.Dimensions, Is.EqualTo(actual.Dimensions));
Assert.That(expected.Data, Is.EquivalentTo(actual.Data));
Assert.NotNull(actual);
Assert.Equal(expected.Dimensions, actual.Dimensions);
Assert.Equal(expected.Data, actual.Data);
}
private void CompareStructureArrays(IStructureArray expected, IStructureArray actual)
private static void CompareStructureArrays(IStructureArray expected, IStructureArray actual)
{
Assert.That(actual, Is.Not.Null);
Assert.That(expected.Dimensions, Is.EqualTo(actual.Dimensions));
Assert.That(expected.FieldNames, Is.EquivalentTo(actual.FieldNames));
Assert.NotNull(actual);
Assert.Equal(expected.Dimensions, actual.Dimensions);
Assert.Equal(expected.FieldNames, actual.FieldNames);
foreach (var name in expected.FieldNames)
{
for (var i = 0; i < expected.Count; i++)
@ -248,31 +267,31 @@ namespace MatFileHandler.Tests
}
}
private void CompareCellArrays(ICellArray expected, ICellArray actual)
private static void CompareCellArrays(ICellArray expected, ICellArray actual)
{
Assert.That(actual, Is.Not.Null);
Assert.That(expected.Dimensions, Is.EqualTo(actual.Dimensions));
Assert.NotNull(actual);
Assert.Equal(expected.Dimensions, actual.Dimensions);
for (var i = 0; i < expected.Count; i++)
{
CompareMatArrays(expected[i], actual[i]);
}
}
private void CompareNumericalArrays<T>(IArrayOf<T> expected, IArrayOf<T> actual)
private static void CompareNumericalArrays<T>(IArrayOf<T> expected, IArrayOf<T> actual)
{
Assert.That(actual, Is.Not.Null);
Assert.That(expected.Dimensions, Is.EqualTo(actual.Dimensions));
Assert.That(expected.Data, Is.EqualTo(actual.Data));
Assert.NotNull(actual);
Assert.Equal(expected.Dimensions, actual.Dimensions);
Assert.Equal(expected.Data, actual.Data);
}
private void CompareCharArrays(ICharArray expected, ICharArray actual)
private static void CompareCharArrays(ICharArray expected, ICharArray actual)
{
Assert.That(actual, Is.Not.Null);
Assert.That(expected.Dimensions, Is.EqualTo(actual.Dimensions));
Assert.That(expected.String, Is.EqualTo(actual.String));
Assert.NotNull(actual);
Assert.Equal(expected.Dimensions, actual.Dimensions);
Assert.Equal(expected.String, actual.String);
}
private void CompareMatArrays(IArray expected, IArray actual)
private static void CompareMatArrays(IArray expected, IArray actual)
{
switch (expected)
{
@ -357,47 +376,47 @@ namespace MatFileHandler.Tests
}
if (expected.IsEmpty)
{
Assert.That(actual.IsEmpty, Is.True);
Assert.True(actual.IsEmpty);
return;
}
throw new NotSupportedException();
}
private void CompareMatFiles(IMatFile expected, IMatFile actual)
private static void CompareMatFiles(IMatFile expected, IMatFile actual)
{
Assert.That(expected.Variables.Length, Is.EqualTo(actual.Variables.Length));
Assert.Equal(expected.Variables.Length, actual.Variables.Length);
for (var i = 0; i < expected.Variables.Length; i++)
{
var expectedVariable = expected.Variables[i];
var actualVariable = actual.Variables[i];
Assert.That(expectedVariable.Name, Is.EqualTo(actualVariable.Name));
Assert.That(expectedVariable.IsGlobal, Is.EqualTo(actualVariable.IsGlobal));
Assert.Equal(expectedVariable.Name, actualVariable.Name);
Assert.Equal(expectedVariable.IsGlobal, actualVariable.IsGlobal);
CompareMatArrays(expectedVariable.Value, actualVariable.Value);
}
}
private void MatCompareWithTestData(string factoryName, string testName, IMatFile actual)
private static void MatCompareWithTestData(
string factoryName,
string testName,
IMatFile actual,
MatFileWritingMethod method,
MatFileWriterOptionsForTests options)
{
var expected = GetMatTestData(factoryName)[testName];
byte[] buffer;
using (var stream = new MemoryStream())
{
var writer = new MatFileWriter(stream);
writer.Write(actual);
buffer = stream.ToArray();
}
using (var stream = new MemoryStream(buffer))
{
var reader = new MatFileReader(stream);
var actualRead = reader.Read();
CompareMatFiles(expected, actualRead);
}
var fullFileName = Path.Combine("test-data", "good", $"{testName}.mat");
var expected = MatFileReadingMethods.ReadMatFile(
MatFileReadingMethod.NormalStream,
fullFileName);
var buffer = MatFileWritingMethods.WriteMatFile(method, options, actual);
using var stream = new MemoryStream(buffer);
var reader = new MatFileReader(stream);
var actualRead = reader.Read();
CompareMatFiles(expected, actualRead);
}
private ComplexOf<T>[] CreateComplexLimits<T>(T[] limits)
where T : struct
private static ComplexOf<T>[] CreateComplexLimits<T>(T[] limits)
where T : struct
{
return new[] { new ComplexOf<T>(limits[0], limits[1]), new ComplexOf<T>(limits[1], limits[0]) };
}
}
}
}

View File

@ -0,0 +1,27 @@
namespace MatFileHandler.Tests;
/// <summary>
/// Method of writing .mat files for testing.
/// </summary>
public enum MatFileWritingMethod
{
/// <summary>
/// Undefined.
/// </summary>
Undefined = 0,
/// <summary>
/// Normal stream (like memory or file stream).
/// </summary>
NormalStream,
/// <summary>
/// A stream that cannot be seeked (like a deflate stream).
/// </summary>
UnseekableStream,
/// <summary>
/// Unaligned stream (what happens if the data don't start at the beginning?).
/// </summary>
UnalignedStream,
}

View File

@ -0,0 +1,58 @@
using System;
using System.IO;
namespace MatFileHandler.Tests;
internal static class MatFileWritingMethods
{
public static byte[] WriteMatFile(MatFileWritingMethod method, MatFileWriterOptionsForTests options, IMatFile matFile)
{
switch (method)
{
case MatFileWritingMethod.NormalStream:
{
using var memoryStream = new MemoryStream();
var matFileWriter = CreateWriter(options, memoryStream);
matFileWriter.Write(matFile);
return memoryStream.ToArray();
}
case MatFileWritingMethod.UnseekableStream:
{
using var memoryStream = new MemoryStream();
using var unseekableStream = new UnseekableWriteStream(memoryStream);
var matFileWriter = CreateWriter(options, unseekableStream);
matFileWriter.Write(matFile);
return memoryStream.ToArray();
}
case MatFileWritingMethod.UnalignedStream:
{
using var memoryStream = new MemoryStream();
memoryStream.Seek(3, SeekOrigin.Begin);
var matFileWriter = CreateWriter(options, memoryStream);
matFileWriter.Write(matFile);
var fullArray = memoryStream.ToArray();
var length = fullArray.Length - 3;
var result = new byte[length];
Buffer.BlockCopy(fullArray, 3, result, 0, length);
return result;
}
default:
throw new NotImplementedException();
}
}
private static MatFileWriter CreateWriter(MatFileWriterOptionsForTests options, Stream stream)
{
return options switch
{
MatFileWriterOptionsForTests.None => new MatFileWriter(stream),
MatFileWriterOptionsForTests.Always => new MatFileWriter(
stream,
new MatFileWriterOptions { UseCompression = CompressionUsage.Always }),
MatFileWriterOptionsForTests.Never => new MatFileWriter(
stream,
new MatFileWriterOptions { UseCompression = CompressionUsage.Never }),
_ => throw new NotImplementedException(),
};
}
}

View File

@ -1,35 +0,0 @@
// Copyright 2017 Alexander Luzgarev
using System.IO;
namespace MatFileHandler.Tests
{
/// <summary>
/// Factory providing the parsed contents of .mat files.
/// </summary>
public class MatTestDataFactory : AbstractTestDataFactory<IMatFile>
{
/// <summary>
/// Initializes a new instance of the <see cref="MatTestDataFactory"/> class.
/// </summary>
/// <param name="testDirectory">Directory containing test files.</param>
public MatTestDataFactory(string testDirectory)
: base(
testDirectory,
new ExtensionTestFilenameConvention("mat"))
{
}
/// <summary>
/// Read and parse data from a .mat file.
/// </summary>
/// <param name="stream">Input stream.</param>
/// <returns>Parsed contents of the file.</returns>
protected override IMatFile ReadDataFromStream(Stream stream)
{
var matFileReader = new MatFileReader(stream);
var matFile = matFileReader.Read();
return matFile;
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.IO;
namespace MatFileHandler.Tests
{
/// <summary>
/// A stream which wraps another stream and only reads one byte at a time,
/// while forbidding seeking in it.
/// </summary>
internal sealed class PartialUnseekableReadStream : Stream
{
private readonly Stream _baseStream;
/// <summary>
/// Initializes a new instance of the <see cref="PartialUnseekableReadStream"/> class.
/// </summary>
/// <param name="baseStream">The stream to wrap.</param>
public PartialUnseekableReadStream(Stream baseStream)
{
_baseStream = baseStream;
}
/// <inheritdoc/>
public override bool CanRead => _baseStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length => _baseStream.Length;
/// <inheritdoc/>
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Flush()
{
_baseStream.Flush();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
return _baseStream.Read(buffer, offset, Math.Min(1, count));
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_baseStream.Dispose();
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.IO;
namespace MatFileHandler.Tests
{
/// <summary>
/// A stream which wraps another stream and forbids seeking in it.
/// </summary>
internal sealed class UnseekableWriteStream : Stream
{
public UnseekableWriteStream(Stream baseStream)
{
_baseStream = baseStream;
}
private readonly Stream _baseStream;
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => _baseStream.CanWrite;
public override long Length => _baseStream.Length;
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
_baseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
_baseStream.Write(buffer, offset, count);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_baseStream.Dispose();
}
base.Dispose(disposing);
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.0.0
MinimumVisualStudioVersion = 10.0.0.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatFileHandler", "MatFileHandler/MatFileHandler.csproj", "{C0CD11D3-016A-4FCD-AF0B-D745F79F3749}"
# Visual Studio Version 16
VisualStudioVersion = 16.0.28621.142
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MatFileHandler", "MatFileHandler\MatFileHandler.csproj", "{C0CD11D3-016A-4FCD-AF0B-D745F79F3749}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatFileHandler.Tests", "MatFileHandler.Tests\MatFileHandler.Tests.csproj", "{4E09DE2D-13D2-458C-BBD2-BE65AAE30CC7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MatFileHandler.Tests", "MatFileHandler.Tests\MatFileHandler.Tests.csproj", "{4E09DE2D-13D2-458C-BBD2-BE65AAE30CC7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2CF58D3D-4CEC-419B-AD67-58665A888BDF}"
ProjectSection(SolutionItems) = preProject
LICENSE.md = LICENSE.md
MatFileHandler\Objects.md = MatFileHandler\Objects.md
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -25,4 +32,7 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {58DB2864-D80D-48D9-B926-7E21D348AA20}
EndGlobalSection
EndGlobal

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
namespace MatFileHandler
@ -85,9 +83,9 @@ namespace MatFileHandler
MxUInt64 = 15,
/// <summary>
/// Undocumented object (?) array type.
/// Undocumented opaque object type.
/// </summary>
MxNewObject = 17,
MxOpaque = 17,
}
/// <summary>
@ -154,4 +152,4 @@ namespace MatFileHandler
/// </summary>
public uint NzMax;
}
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.IO;
namespace MatFileHandler
{
/// <summary>
/// A stream that calculates Adler32 checksum of everything
/// written to it before passing to another stream.
/// </summary>
internal class ChecksumCalculatingStream : Stream
{
private const uint BigPrime = 0xFFF1;
private readonly Stream _stream;
private uint s1;
private uint s2;
/// <summary>
/// Initializes a new instance of the <see cref="ChecksumCalculatingStream"/> class.
/// </summary>
/// <param name="stream">Wrapped stream.</param>
public ChecksumCalculatingStream(Stream stream)
{
_stream = stream;
s1 = 1;
s2 = 0;
}
/// <inheritdoc />
public override bool CanRead => false;
/// <inheritdoc />
public override bool CanSeek => false;
/// <inheritdoc />
public override bool CanWrite => true;
/// <inheritdoc />
public override long Length => throw new NotImplementedException();
/// <inheritdoc />
public override long Position
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
/// <inheritdoc />
public override void Flush()
{
_stream.Flush();
}
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void SetLength(long value)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
for (var i = offset; i < offset + count; i++)
{
s1 = (s1 + buffer[i]) % BigPrime;
s2 = (s2 + s1) % BigPrime;
}
_stream.Write(buffer, offset, count);
}
/// <summary>
/// Calculate the checksum of everything written to the stream so far.
/// </summary>
/// <returns>Checksum of everything written to the stream so far.</returns>
public uint GetCrc()
{
return (s2 << 16) | s1;
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
namespace MatFileHandler
@ -8,7 +6,7 @@ namespace MatFileHandler
/// A structure representing a complex number where real and imaginary parts are of type T.
/// </summary>
/// <typeparam name="T">Type of real and imaginary parts.</typeparam>
public struct ComplexOf<T> : IEquatable<ComplexOf<T>>
public readonly struct ComplexOf<T> : IEquatable<ComplexOf<T>>
where T : struct
{
/// <summary>
@ -71,10 +69,11 @@ namespace MatFileHandler
/// <returns>True iff another object is a complex number equal to this.</returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
if (obj is null)
{
return false;
}
return obj is ComplexOf<T> other && Equals(other);
}
@ -90,4 +89,4 @@ namespace MatFileHandler
}
}
}
}
}

View File

@ -0,0 +1,18 @@
namespace MatFileHandler
{
/// <summary>
/// Describes compression usage strategy for writing files.
/// </summary>
public enum CompressionUsage
{
/// <summary>
/// Never use compression.
/// </summary>
Never,
/// <summary>
/// Always use compression.
/// </summary>
Always,
}
}

View File

@ -1,10 +1,9 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
#pragma warning disable CA1822
namespace MatFileHandler
{
/// <summary>
@ -52,7 +51,7 @@ namespace MatFileHandler
{
if (data.Length != dimensions.NumberOfElements())
{
throw new ArgumentException("Data size does not match the specified dimensions", "data");
throw new ArgumentException("Data size does not match the specified dimensions", nameof(data));
}
return new MatNumericalArrayOf<T>(GetStandardFlags<T>(), dimensions, string.Empty, data);
}
@ -157,7 +156,7 @@ namespace MatFileHandler
return new MatFile(variables);
}
private ArrayFlags ConstructArrayFlags(ArrayType class_, bool isComplex = false, bool isLogical = false)
private static ArrayFlags ConstructArrayFlags(ArrayType class_, bool isComplex = false, bool isLogical = false)
{
return new ArrayFlags
{
@ -167,7 +166,7 @@ namespace MatFileHandler
};
}
private ArrayFlags GetStandardFlags<T>()
private static ArrayFlags GetStandardFlags<T>()
{
if (typeof(T) == typeof(sbyte))
{
@ -266,4 +265,4 @@ namespace MatFileHandler
};
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -8,4 +6,4 @@ namespace MatFileHandler
internal class DataElement
{
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.Linq;
@ -33,8 +31,8 @@ namespace MatFileHandler
DataElement data,
DataElement imaginaryData)
{
var realParts = DataExtraction.GetDataAsDouble(data).ToArrayLazily();
var imaginaryParts = DataExtraction.GetDataAsDouble(imaginaryData).ToArrayLazily();
var realParts = DataExtraction.GetDataAsDouble(data);
var imaginaryParts = DataExtraction.GetDataAsDouble(imaginaryData);
if (realParts == null)
{
throw new HandlerException("Couldn't read sparse array.");
@ -71,16 +69,21 @@ namespace MatFileHandler
{
throw new NotSupportedException("Only 2-dimensional sparse arrays are supported");
}
if (data == null)
if (data is null)
{
throw new ArgumentException("Null data found.", "data");
throw new ArgumentException("Null data found.", nameof(data));
}
var elements =
var maybeElements =
ConvertDataToSparseProperType<T>(data, flags.ArrayFlags.Variable.HasFlag(Variable.IsLogical));
if (elements == null)
if (maybeElements is not { } elements)
{
throw new HandlerException("Couldn't read sparse array.");
}
var dataDictionary =
ConvertMatlabSparseToDictionary(rowIndex, columnIndex, j => elements[j]);
return new MatSparseArrayOf<T>(flags, dimensions, name, dataDictionary);
@ -106,26 +109,23 @@ namespace MatFileHandler
int[] dimensions,
string name,
DataElement realData,
DataElement imaginaryData)
DataElement? imaginaryData)
where T : struct
{
if (flags.Variable.HasFlag(Variable.IsLogical))
{
var data = DataExtraction.GetDataAsUInt8(realData).ToArrayLazily().Select(x => x != 0).ToArray();
var data = DataExtraction.GetDataAsUInt8(realData).Select(x => x != 0).ToArray();
return new MatNumericalArrayOf<bool>(flags, dimensions, name, data);
}
switch (flags.Class)
{
case ArrayType.MxChar:
switch (realData)
return realData switch
{
case MiNum<byte> dataByte:
return ConvertToMatCharArray(flags, dimensions, name, dataByte);
case MiNum<ushort> dataUshort:
return ConvertToMatCharArray(flags, dimensions, name, dataUshort);
default:
throw new NotSupportedException("Only utf8, utf16 or ushort char arrays are supported.");
}
MiNum<byte> dataByte => ConvertToMatCharArray(flags, dimensions, name, dataByte),
MiNum<ushort> dataUshort => ConvertToMatCharArray(flags, dimensions, name, dataUshort),
_ => throw new NotSupportedException("Only utf8, utf16 or ushort char arrays are supported."),
};
case ArrayType.MxDouble:
case ArrayType.MxSingle:
case ArrayType.MxInt8:
@ -139,6 +139,11 @@ namespace MatFileHandler
var dataArray = ConvertDataToProperType<T>(realData, flags.Class);
if (flags.Variable.HasFlag(Variable.IsComplex))
{
if (imaginaryData is null)
{
throw new HandlerException("Imaginary part of a complex variable not found.");
}
var dataArray2 = ConvertDataToProperType<T>(imaginaryData, flags.Class);
if (flags.Class == ArrayType.MxDouble)
{
@ -163,52 +168,28 @@ namespace MatFileHandler
string name,
MiNum<byte> dataElement)
{
var data = dataElement?.Data;
var data = dataElement.Data;
return new MatCharArrayOf<byte>(flags, dimensions, name, data, Encoding.UTF8.GetString(data));
}
private static T[] ConvertDataToProperType<T>(DataElement data, ArrayType arrayType)
{
switch (arrayType)
{
case ArrayType.MxDouble:
return DataExtraction.GetDataAsDouble(data).ToArrayLazily() as T[];
case ArrayType.MxSingle:
return DataExtraction.GetDataAsSingle(data).ToArrayLazily() as T[];
case ArrayType.MxInt8:
return DataExtraction.GetDataAsInt8(data).ToArrayLazily() as T[];
case ArrayType.MxUInt8:
return DataExtraction.GetDataAsUInt8(data).ToArrayLazily() as T[];
case ArrayType.MxInt16:
return DataExtraction.GetDataAsInt16(data).ToArrayLazily() as T[];
case ArrayType.MxUInt16:
return DataExtraction.GetDataAsUInt16(data).ToArrayLazily() as T[];
case ArrayType.MxInt32:
return DataExtraction.GetDataAsInt32(data).ToArrayLazily() as T[];
case ArrayType.MxUInt32:
return DataExtraction.GetDataAsUInt32(data).ToArrayLazily() as T[];
case ArrayType.MxInt64:
return DataExtraction.GetDataAsInt64(data).ToArrayLazily() as T[];
case ArrayType.MxUInt64:
return DataExtraction.GetDataAsUInt64(data).ToArrayLazily() as T[];
default:
throw new NotSupportedException();
}
return TryConvertDataToProperType<T>(data, arrayType)
?? throw new HandlerException($"Unexpected data type.");
}
private static T[] ConvertDataToSparseProperType<T>(DataElement data, bool isLogical)
private static T[]? ConvertDataToSparseProperType<T>(DataElement data, bool isLogical)
{
if (isLogical)
{
return DataExtraction.GetDataAsUInt8(data).ToArrayLazily().Select(x => x != 0).ToArray() as T[];
return DataExtraction.GetDataAsUInt8(data).Select(x => x != 0).ToArray() as T[];
}
switch (data)
return data switch
{
case MiNum<double> _:
return DataExtraction.GetDataAsDouble(data).ToArrayLazily() as T[];
default:
throw new NotSupportedException();
}
MiNum<double> => DataExtraction.GetDataAsDouble(data) as T[],
_ => throw new NotSupportedException(),
};
}
private static MatCharArrayOf<ushort> ConvertToMatCharArray(
@ -217,7 +198,7 @@ namespace MatFileHandler
string name,
MiNum<ushort> dataElement)
{
var data = dataElement?.Data;
var data = dataElement.Data;
return new MatCharArrayOf<ushort>(
flags,
dimensions,
@ -226,7 +207,7 @@ namespace MatFileHandler
new string(data.Select(x => (char)x).ToArray()));
}
private static Dictionary<(int, int), T> ConvertMatlabSparseToDictionary<T>(
private static Dictionary<(int row, int column), T> ConvertMatlabSparseToDictionary<T>(
int[] rowIndex,
int[] columnIndex,
Func<int, T> get)
@ -242,5 +223,23 @@ namespace MatFileHandler
}
return result;
}
private static T[]? TryConvertDataToProperType<T>(DataElement data, ArrayType arrayType)
{
return arrayType switch
{
ArrayType.MxDouble => DataExtraction.GetDataAsDouble(data) as T[],
ArrayType.MxSingle => DataExtraction.GetDataAsSingle(data) as T[],
ArrayType.MxInt8 => DataExtraction.GetDataAsInt8(data) as T[],
ArrayType.MxUInt8 => DataExtraction.GetDataAsUInt8(data) as T[],
ArrayType.MxInt16 => DataExtraction.GetDataAsInt16(data) as T[],
ArrayType.MxUInt16 => DataExtraction.GetDataAsUInt16(data) as T[],
ArrayType.MxInt32 => DataExtraction.GetDataAsInt32(data) as T[],
ArrayType.MxUInt32 => DataExtraction.GetDataAsUInt32(data) as T[],
ArrayType.MxInt64 => DataExtraction.GetDataAsInt64(data) as T[],
ArrayType.MxUInt64 => DataExtraction.GetDataAsUInt64(data) as T[],
_ => throw new NotSupportedException()
};
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.IO;
@ -12,60 +10,51 @@ namespace MatFileHandler
/// <summary>
/// Functions for reading data elements from a .mat file.
/// </summary>
internal static class DataElementReader
internal class DataElementReader
{
private readonly SubsystemData subsystemData;
/// <summary>
/// Initializes a new instance of the <see cref="DataElementReader"/> class.
/// </summary>
/// <param name="subsystemData">Reference to file's SubsystemData.</param>
public DataElementReader(SubsystemData subsystemData)
{
this.subsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData));
}
/// <summary>
/// Read a data element.
/// </summary>
/// <param name="reader">Input reader.</param>
/// <returns>Data element.</returns>
public static DataElement Read(BinaryReader reader)
public DataElement? Read(BinaryReader reader)
{
var (dataReader, tag) = ReadTag(reader);
DataElement result;
switch (tag.Type)
var maybeTagPair = ReadTag(reader);
if (maybeTagPair is not { } tagPair)
{
case DataType.MiInt8:
result = ReadNum<sbyte>(tag, dataReader);
break;
case DataType.MiUInt8:
case DataType.MiUtf8:
result = ReadNum<byte>(tag, dataReader);
break;
case DataType.MiInt16:
result = ReadNum<short>(tag, dataReader);
break;
case DataType.MiUInt16:
case DataType.MiUtf16:
result = ReadNum<ushort>(tag, dataReader);
break;
case DataType.MiInt32:
result = ReadNum<int>(tag, dataReader);
break;
case DataType.MiUInt32:
result = ReadNum<uint>(tag, dataReader);
break;
case DataType.MiSingle:
result = ReadNum<float>(tag, dataReader);
break;
case DataType.MiDouble:
result = ReadNum<double>(tag, dataReader);
break;
case DataType.MiInt64:
result = ReadNum<long>(tag, dataReader);
break;
case DataType.MiUInt64:
result = ReadNum<ulong>(tag, dataReader);
break;
case DataType.MiMatrix:
result = ReadMatrix(tag, dataReader);
break;
case DataType.MiCompressed:
result = ReadCompressed(tag, dataReader);
break;
default:
throw new NotSupportedException("Unknown element.");
return null;
}
var (dataReader, tag) = tagPair;
var result = tag.Type switch
{
DataType.MiInt8 => ReadNum<sbyte>(tag, dataReader),
DataType.MiUInt8 or DataType.MiUtf8 => ReadNum<byte>(tag, dataReader),
DataType.MiInt16 => ReadNum<short>(tag, dataReader),
DataType.MiUInt16 or DataType.MiUtf16 => ReadNum<ushort>(tag, dataReader),
DataType.MiInt32 => ReadNum<int>(tag, dataReader),
DataType.MiUInt32 => ReadNum<uint>(tag, dataReader),
DataType.MiSingle => ReadNum<float>(tag, dataReader),
DataType.MiDouble => ReadNum<double>(tag, dataReader),
DataType.MiInt64 => ReadNum<long>(tag, dataReader),
DataType.MiUInt64 => ReadNum<ulong>(tag, dataReader),
DataType.MiMatrix => ReadMatrix(tag, dataReader),
DataType.MiCompressed => ReadCompressed(tag, dataReader),
_ => throw new NotSupportedException("Unknown element."),
};
if (tag.Type != DataType.MiCompressed)
{
var position = reader.BaseStream.Position;
@ -74,31 +63,43 @@ namespace MatFileHandler
reader.ReadBytes(8 - (int)(position % 8));
}
}
return result;
}
private static (BinaryReader, Tag) ReadTag(BinaryReader reader)
/// <summary>
/// Parse opaque link data.
/// </summary>
/// <param name="data">Opaque link data.</param>
/// <returns>Dimensions array, links array, class index.</returns>
internal static (int[] dimensions, int[] links, int classIndex) ParseOpaqueData(uint[] data)
{
var type = reader.ReadInt32();
var typeHi = type >> 16;
if (typeHi == 0)
var nDims = data[1];
var dimensions = new int[nDims];
var position = 2;
for (var i = 0; i < nDims; i++)
{
var length = reader.ReadInt32();
return (reader, new Tag((DataType)type, length));
dimensions[i] = (int)data[position];
position++;
}
else
var count = dimensions.NumberOfElements();
var links = new int[count];
for (var i = 0; i < count; i++)
{
var length = typeHi;
type = type & 0xffff;
var smallReader = new BinaryReader(new MemoryStream(reader.ReadBytes(4)));
return (smallReader, new Tag((DataType)type, length));
links[i] = (int)data[position];
position++;
}
var classIndex = (int)data[position];
return (dimensions, links, classIndex);
}
private static ArrayFlags ReadArrayFlags(DataElement element)
{
var flagData = (element as MiNum<uint>)?.Data ??
throw new HandlerException("Unexpected type in array flags.");
throw new HandlerException("Unexpected type in array flags.");
var class_ = (ArrayType)(flagData[0] & 0xff);
var variableFlags = (flagData[0] >> 8) & 0x0e;
return new ArrayFlags
@ -108,45 +109,14 @@ namespace MatFileHandler
};
}
private static SparseArrayFlags ReadSparseArrayFlags(DataElement element)
{
var arrayFlags = ReadArrayFlags(element);
var flagData = (element as MiNum<uint>)?.Data ??
throw new HandlerException("Unexpected type in sparse array flags.");
var nzMax = flagData[1];
return new SparseArrayFlags
{
ArrayFlags = arrayFlags,
NzMax = nzMax,
};
}
private static int[] ReadDimensionsArray(MiNum<int> element)
{
return element.Data;
}
private static DataElement ReadData(DataElement element)
{
return element;
}
private static string ReadName(MiNum<sbyte> element)
private static int[] ReadDimensionsArray(MiNum<int> element)
{
return Encoding.ASCII.GetString(element.Data.Select(x => (byte)x).ToArray());
}
private static DataElement ReadNum<T>(Tag tag, BinaryReader reader)
where T : struct
{
var bytes = reader.ReadBytes(tag.Length);
if (tag.Type == DataType.MiUInt8)
{
return new MiNum<byte>(bytes);
}
var result = new T[bytes.Length / tag.ElementSize];
Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length);
return new MiNum<T>(result);
return element.Data;
}
private static string[] ReadFieldNames(MiNum<sbyte> element, int fieldNameLength)
@ -162,21 +132,147 @@ namespace MatFileHandler
list.Add((byte)element.Data[position]);
position++;
}
result[i] = Encoding.ASCII.GetString(list.ToArray());
}
return result;
}
private static DataElement ContinueReadingSparseArray(
private static string ReadName(MiNum<sbyte> element)
{
return Encoding.ASCII.GetString(element.Data.Select(x => (byte)x).ToArray());
}
private static DataElement ReadNum<T>(Tag tag, BinaryReader reader)
where T : struct
{
var bytes = reader.ReadBytes(tag.Length);
if (tag.Type == DataType.MiUInt8)
{
return new MiNum<byte>(bytes);
}
var result = new T[bytes.Length / tag.ElementSize];
Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length);
return new MiNum<T>(result);
}
private static SparseArrayFlags ReadSparseArrayFlags(DataElement element)
{
var arrayFlags = ReadArrayFlags(element);
var flagData = (element as MiNum<uint>)?.Data ??
throw new HandlerException("Unexpected type in sparse array flags.");
var nzMax = flagData[1];
return new SparseArrayFlags
{
ArrayFlags = arrayFlags,
NzMax = nzMax,
};
}
private static int? TryReadInt32(BinaryReader reader)
{
var buffer = new byte[4];
var position = 0;
while (position < 4)
{
var actually = reader.BaseStream.Read(buffer, position, 4 - position);
if (actually == 0)
{
return null;
}
position += actually;
}
return BitConverter.ToInt32(buffer, 0);
}
private static (BinaryReader reader, Tag tag)? ReadTag(BinaryReader reader)
{
var maybeType = TryReadInt32(reader);
if (maybeType is not int type)
{
return null;
}
var typeHi = type >> 16;
if (typeHi == 0)
{
var length = reader.ReadInt32();
return (reader, new Tag((DataType)type, length));
}
else
{
var length = typeHi;
type &= 0xffff;
var smallReader = new BinaryReader(new MemoryStream(reader.ReadBytes(4)));
return (smallReader, new Tag((DataType)type, length));
}
}
private MatCellArray ContinueReadingCellArray(
BinaryReader reader,
ArrayFlags flags,
int[] dimensions,
string name)
{
var numberOfElements = dimensions.NumberOfElements();
var elements = new List<IArray>();
for (var i = 0; i < numberOfElements; i++)
{
var element = Read(reader) as IArray
?? throw new HandlerException("Unable to read cell array.");
elements.Add(element);
}
return new MatCellArray(flags, dimensions, name, elements);
}
private DataElement ContinueReadingOpaque(BinaryReader reader)
{
var nameElement = Read(reader) as MiNum<sbyte> ??
throw new HandlerException("Unexpected type in object name.");
var name = ReadName(nameElement);
var anotherElement = Read(reader) as MiNum<sbyte> ??
throw new HandlerException("Unexpected type in object type description.");
var typeDescription = ReadName(anotherElement);
var classNameElement = Read(reader) as MiNum<sbyte> ??
throw new HandlerException("Unexpected type in class name.");
var className = ReadName(classNameElement);
var dataElement = Read(reader) ?? throw new HandlerException("Missing opaque data element.");
var data = ReadData(dataElement);
if (data is MatNumericalArrayOf<uint> linkElement)
{
var (dimensions, indexToObjectId, classIndex) = ParseOpaqueData(linkElement.Data);
return new OpaqueLink(
name,
typeDescription,
className,
dimensions,
data,
indexToObjectId,
classIndex,
subsystemData);
}
else
{
return new Opaque(name, typeDescription, className, Array.Empty<int>(), data, subsystemData);
}
}
private MatArray ContinueReadingSparseArray(
BinaryReader reader,
DataElement firstElement,
int[] dimensions,
string name)
{
var sparseArrayFlags = ReadSparseArrayFlags(firstElement);
var rowIndex = Read(reader) as MiNum<int> ?? throw new HandlerException("Unexpected type in row indices of a sparse array.");
var columnIndex = Read(reader) as MiNum<int> ?? throw new HandlerException("Unexpected type in column indices of a sparse array.");
var data = Read(reader);
var rowIndex = Read(reader) as MiNum<int> ??
throw new HandlerException("Unexpected type in row indices of a sparse array.");
var columnIndex = Read(reader) as MiNum<int> ??
throw new HandlerException("Unexpected type in column indices of a sparse array.");
var data = Read(reader) ?? throw new HandlerException("Missing sparse array data.");
if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsLogical))
{
return DataElementConverter.ConvertToMatSparseArrayOf<bool>(
@ -187,9 +283,10 @@ namespace MatFileHandler
columnIndex.Data,
data);
}
if (sparseArrayFlags.ArrayFlags.Variable.HasFlag(Variable.IsComplex))
{
var imaginaryData = Read(reader);
var imaginaryData = Read(reader) ?? throw new HandlerException("Missing imaginary part of sparse array data.");
return DataElementConverter.ConvertToMatSparseArrayOfComplex(
sparseArrayFlags,
dimensions,
@ -199,81 +296,95 @@ namespace MatFileHandler
data,
imaginaryData);
}
switch (data)
return data switch
{
case MiNum<double> _:
return DataElementConverter.ConvertToMatSparseArrayOf<double>(
sparseArrayFlags,
dimensions,
name,
rowIndex.Data,
columnIndex.Data,
data);
default:
throw new NotSupportedException("Only double and logical sparse arrays are supported.");
}
MiNum<double> => DataElementConverter.ConvertToMatSparseArrayOf<double>(
sparseArrayFlags,
dimensions,
name,
rowIndex.Data,
columnIndex.Data,
data),
_ => throw new NotSupportedException("Only double and logical sparse arrays are supported."),
};
}
private static DataElement ContinueReadingCellArray(
BinaryReader reader,
ArrayFlags flags,
int[] dimensions,
string name)
{
var numberOfElements = dimensions.NumberOfElements();
var elements = new List<IArray>();
for (var i = 0; i < numberOfElements; i++)
{
var element = Read(reader) as IArray;
elements.Add(element);
}
return new MatCellArray(flags, dimensions, name, elements);
}
private static DataElement ContinueReadingStructure(
private MatStructureArray ContinueReadingStructure(
BinaryReader reader,
ArrayFlags flags,
int[] dimensions,
string name,
int fieldNameLength)
{
var element = Read(reader);
var fieldNames = ReadFieldNames(element as MiNum<sbyte>, fieldNameLength);
var element = Read(reader) as MiNum<sbyte>
?? throw new HandlerException("Unable to parse structure field names.");
var fieldNames = ReadFieldNames(element, fieldNameLength);
var fields = new Dictionary<string, List<IArray>>();
foreach (var fieldName in fieldNames)
{
fields[fieldName] = new List<IArray>();
}
var numberOfElements = dimensions.NumberOfElements();
for (var i = 0; i < numberOfElements; i++)
{
foreach (var fieldName in fieldNames)
{
var field = Read(reader) as IArray;
var field = Read(reader) as IArray
?? throw new HandlerException("Unable to parse field name.");
fields[fieldName].Add(field);
}
}
return new MatStructureArray(flags, dimensions, name, fields);
}
private static DataElement ContinueReadingNewObject()
private DataElement ReadCompressed(Tag tag, BinaryReader reader)
{
throw new HandlerException("Cannot read objects.");
reader.ReadBytes(2);
DataElement element;
using (var substream = new Substream(reader.BaseStream, tag.Length - 6))
{
using (var deflateStream = new DeflateStream(substream, CompressionMode.Decompress))
using (var bufferedStream = new BufferedStream(deflateStream))
using (var positionTrackingStream = new PositionTrackingStream(bufferedStream))
using (var innerReader = new BinaryReader(positionTrackingStream))
{
element = Read(innerReader) ?? throw new HandlerException("Missing compressed data.");
}
if (substream.Position != substream.Length)
{
// In the pathological case that the deflate stream did not read the full
// length, then read out the rest manually (normally 1 byte).
reader.ReadBytes((int)(substream.Length - substream.Position));
}
}
reader.ReadBytes(4);
return element;
}
private static DataElement ReadMatrix(Tag tag, BinaryReader reader)
private DataElement ReadMatrix(Tag tag, BinaryReader reader)
{
if (tag.Length == 0)
{
return MatArray.Empty();
}
var element1 = Read(reader);
var element1 = Read(reader) ?? throw new HandlerException("Missing matrix data.");
var flags = ReadArrayFlags(element1);
if (flags.Class == ArrayType.MxNewObject)
if (flags.Class == ArrayType.MxOpaque)
{
return ContinueReadingNewObject();
return ContinueReadingOpaque(reader);
}
var element2 = Read(reader) as MiNum<int> ?? throw new HandlerException("Unexpected type in array dimensions data.");
var element2 = Read(reader) as MiNum<int> ??
throw new HandlerException("Unexpected type in array dimensions data.");
var dimensions = ReadDimensionsArray(element2);
var element3 = Read(reader) as MiNum<sbyte> ?? throw new HandlerException("Unexpected type in array name.");
var name = ReadName(element3);
@ -281,47 +392,49 @@ namespace MatFileHandler
{
return ContinueReadingCellArray(reader, flags, dimensions, name);
}
if (flags.Class == ArrayType.MxSparse)
{
return ContinueReadingSparseArray(reader, element1, dimensions, name);
}
var element4 = Read(reader);
var element4 = Read(reader) ?? throw new HandlerException("Missing matrix data.");
var data = ReadData(element4);
DataElement imaginaryData = null;
DataElement? imaginaryData = null;
if (flags.Variable.HasFlag(Variable.IsComplex))
{
var element5 = Read(reader);
var element5 = Read(reader) ?? throw new HandlerException("Missing complex matrix data.");
imaginaryData = ReadData(element5);
}
if (flags.Class == ArrayType.MxStruct)
{
var fieldNameLengthElement = data as MiNum<int> ??
throw new HandlerException("Unexpected type in structure field name length.");
throw new HandlerException(
"Unexpected type in structure field name length.");
return ContinueReadingStructure(reader, flags, dimensions, name, fieldNameLengthElement.Data[0]);
}
switch (flags.Class)
{
case ArrayType.MxChar:
switch (data)
return data switch
{
case MiNum<byte> _:
return DataElementConverter.ConvertToMatNumericalArrayOf<byte>(
flags,
dimensions,
name,
data,
imaginaryData);
case MiNum<ushort> _:
return DataElementConverter.ConvertToMatNumericalArrayOf<ushort>(
flags,
dimensions,
name,
data,
imaginaryData);
default:
throw new NotSupportedException(
$"This type of char array ({data.GetType()}) is not supported.");
}
MiNum<byte> => DataElementConverter.ConvertToMatNumericalArrayOf<byte>(
flags,
dimensions,
name,
data,
imaginaryData),
MiNum<ushort> => DataElementConverter.ConvertToMatNumericalArrayOf<ushort>(
flags,
dimensions,
name,
data,
imaginaryData),
_ => throw new NotSupportedException(
$"This type of char array ({data.GetType()}) is not supported."),
};
case ArrayType.MxInt8:
return DataElementConverter.ConvertToMatNumericalArrayOf<sbyte>(
flags,
@ -339,6 +452,7 @@ namespace MatFileHandler
data,
imaginaryData);
}
return DataElementConverter.ConvertToMatNumericalArrayOf<byte>(
flags,
dimensions,
@ -405,31 +519,5 @@ namespace MatFileHandler
throw new HandlerException("Unknown data type.");
}
}
private static DataElement ReadCompressed(Tag tag, BinaryReader reader)
{
reader.ReadBytes(2);
var compressedData = new byte[tag.Length - 6];
reader.BaseStream.Read(compressedData, 0, tag.Length - 6);
reader.ReadBytes(4);
var resultStream = new MemoryStream();
using (var compressedStream = new MemoryStream(compressedData))
{
using (var stream = new DeflateStream(compressedStream, CompressionMode.Decompress, leaveOpen: true))
{
stream.CopyTo(resultStream);
}
}
resultStream.Position = 0;
return Read(resultStream);
}
private static DataElement Read(Stream stream)
{
using (var reader = new BinaryReader(stream))
{
return Read(reader);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
namespace MatFileHandler
@ -16,41 +14,21 @@ namespace MatFileHandler
/// <returns>Size in bytes.</returns>
public static int Size(this DataType type)
{
switch (type)
return type switch
{
case DataType.MiInt8:
return 1;
case DataType.MiUInt8:
return 1;
case DataType.MiInt16:
return 2;
case DataType.MiUInt16:
return 2;
case DataType.MiInt32:
return 4;
case DataType.MiUInt32:
return 4;
case DataType.MiSingle:
return 4;
case DataType.MiDouble:
return 8;
case DataType.MiInt64:
return 8;
case DataType.MiUInt64:
return 8;
case DataType.MiMatrix:
return 0;
case DataType.MiCompressed:
return 0;
case DataType.MiUtf8:
return 1;
case DataType.MiUtf16:
return 2;
case DataType.MiUtf32:
return 4;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
DataType.MiInt8 or DataType.MiUInt8 => 1,
DataType.MiInt16 or DataType.MiUInt16 => 2,
DataType.MiInt32 or DataType.MiUInt32 => 4,
DataType.MiSingle => 4,
DataType.MiDouble => 8,
DataType.MiInt64 or DataType.MiUInt64 => 8,
DataType.MiMatrix => 0,
DataType.MiCompressed => 0,
DataType.MiUtf8 => 1,
DataType.MiUtf16 => 2,
DataType.MiUtf32 => 4,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using System.Linq;
using System.Numerics;
namespace MatFileHandler
{
/// <summary>
/// A better interface for using datetime objects.
/// </summary>
public class DatetimeAdapter
{
private readonly double[] data;
private readonly DateTimeOffset epoch;
/// <summary>
/// Initializes a new instance of the <see cref="DatetimeAdapter"/> class.
/// </summary>
/// <param name="array">Source datetime object.</param>
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);
switch (matObject["data", 0])
{
case IArrayOf<double> dataArray:
data = dataArray.ConvertToDoubleArray()
?? throw new HandlerException("Cannot extract data for the datetime adapter.");
Dimensions = dataArray.Dimensions;
break;
case IArrayOf<Complex> dataComplex:
var complexData = dataComplex.ConvertToComplexArray()
?? throw new HandlerException("Cannot extract data for the datetime adapter.");
data = complexData.Select(c => c.Real).ToArray();
Dimensions = dataComplex.Dimensions;
break;
default:
throw new HandlerException("Datetime data not found.");
}
}
/// <summary>
/// Gets datetime array dimensions.
/// </summary>
public int[] Dimensions { get; }
/// <summary>
/// Gets values of datetime object at given position in the array converted to <see cref="DateTimeOffset"/>.
/// </summary>
/// <param name="list">Indices.</param>
/// <returns>Value converted to <see cref="DateTimeOffset"/>; null if the resulting value is unrepresentable.</returns>
public DateTimeOffset? this[params int[] list]
{
get
{
var milliseconds = data[Dimensions.DimFlatten(list)];
return milliseconds switch
{
< -62_135_596_800_000.0 or > 253_402_300_799_999.0 => null,
_ => epoch.AddMilliseconds(milliseconds),
};
}
}
}
}

View File

@ -1,5 +1,4 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Linq;
namespace MatFileHandler
@ -36,5 +35,48 @@ namespace MatFileHandler
{
return dimensions.Aggregate(1, (x, y) => x * y);
}
/// <summary>
/// Rearrange data from a flat (column-major) array into a multi-dimensional array.
/// </summary>
/// <typeparam name="T">Array element type.</typeparam>
/// <param name="dimensions">Target array dimensions.</param>
/// <param name="data">Flat (column-major) data to rearrange.</param>
/// <returns>An array of specified dimensions containing data from the original flat array, layed out according to column-major order.</returns>
public static Array UnflattenArray<T>(this int[] dimensions, T[] data)
{
var result = Array.CreateInstance(typeof(T), dimensions);
var n = dimensions.NumberOfElements();
var indices = new int[dimensions.Length];
for (var i = 0; i < n; i++)
{
result.SetValue(data[i], indices);
IncrementMultiIndex(dimensions, indices);
}
return result;
}
private static void IncrementMultiIndex(int[] dimensions, int[] indices)
{
var currentPosition = 0;
while (true)
{
if (currentPosition >= indices.Length)
{
break;
}
indices[currentPosition]++;
if (indices[currentPosition] >= dimensions[currentPosition])
{
indices[currentPosition] = 0;
currentPosition++;
}
else
{
break;
}
}
}
}
}
}

View File

@ -0,0 +1,42 @@
using System;
namespace MatFileHandler
{
/// <summary>
/// A better interface for using duration objects.
/// </summary>
public class DurationAdapter
{
private readonly double[] data;
/// <summary>
/// Initializes a new instance of the <see cref="DurationAdapter"/> class.
/// </summary>
/// <param name="array">Source duration object.</param>
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()
?? throw new HandlerException("Cannot extract data for the duration adapter.");
Dimensions = dataObject.Dimensions;
}
/// <summary>
/// Gets duration array dimensions.
/// </summary>
public int[] Dimensions { get; }
/// <summary>
/// Gets duration object at given position.
/// </summary>
/// <param name="list">Indices.</param>
/// <returns>Value.</returns>
public TimeSpan this[params int[] list] => TimeSpan.FromTicks((long)(10000.0 * data[Dimensions.DimFlatten(list)]));
}
}

View File

@ -0,0 +1,58 @@
namespace MatFileHandler
{
/// <summary>
/// A better interface for using enum adapter.
/// </summary>
public class EnumAdapter
{
/// <summary>
/// Initializes a new instance of the <see cref="EnumAdapter"/> class.
/// </summary>
/// <param name="array">Source enum object.</param>
public EnumAdapter(IArray array)
{
var matObject = array as Opaque;
if (matObject?.RawData is not IStructureArray rawData)
{
throw new HandlerException("Cannot extract data for the enum adapter.");
}
if (rawData["ValueNames"] is not IArrayOf<uint> valueNamesData)
{
throw new HandlerException("Cannot extract data for the enum adapter.");
}
var numberOfNames = valueNamesData.Count;
var valueNames = new string[numberOfNames];
var names = matObject.SubsystemData.FieldNames;
for (var i = 0; i < numberOfNames; i++)
{
valueNames[i] = names[valueNamesData[i] - 1];
}
if (rawData["ValueIndices"] is not IArrayOf<uint> valueIndices)
{
throw new HandlerException("Cannot extract data for the enum adapter.");
}
ClassName = matObject.ClassName;
ValueNames = valueNames;
Values = valueIndices;
}
/// <summary>
/// Gets name of the enumeration class.
/// </summary>
public string ClassName { get; }
/// <summary>
/// Gets names of the enumeration values.
/// </summary>
public string[] ValueNames { get; }
/// <summary>
/// Gets indices of values stored in the variable.
/// </summary>
public IArrayOf<uint> Values { get; }
}
}

View File

@ -0,0 +1,391 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
namespace MatFileHandler
{
/// <summary>
/// A simulated writer of .mat files that just calculate the length of data that would be written.
/// </summary>
internal class FakeWriter
{
/// <summary>
/// Gets current position of the writer.
/// </summary>
public int Position { get; private set; }
/// <summary>
/// Write contents of a numerical array.
/// </summary>
/// <param name="array">A numerical array.</param>
/// <param name="name">Name of the array.</param>
public void WriteNumericalArrayContents(IArray array, string name)
{
WriteArrayFlags();
WriteDimensions(array.Dimensions);
WriteName(name);
WriteNumericalArrayValues(array);
}
/// <summary>
/// Write contents of a char array.
/// </summary>
/// <param name="charArray">A char array.</param>
/// <param name="name">Name of the array.</param>
public void WriteCharArrayContents(ICharArray charArray, string name)
{
WriteArrayFlags();
WriteDimensions(charArray.Dimensions);
WriteName(name);
WriteDataElement(GetLengthOfByteArray<ushort>(charArray.String.Length));
}
/// <summary>
/// Write contents of a sparse array.
/// </summary>
/// <typeparam name="T">Array element type.</typeparam>
/// <param name="array">A sparse array.</param>
/// <param name="name">Name of the array.</param>
public void WriteSparseArrayContents<T>(
ISparseArrayOf<T> array,
string name)
where T : unmanaged, IEquatable<T>
{
(var rowsLength, var columnsLength, var dataLength, var nonZero) = PrepareSparseArrayData(array);
WriteSparseArrayFlags();
WriteDimensions(array.Dimensions);
WriteName(name);
WriteSparseArrayValues<T>(rowsLength, columnsLength, dataLength);
}
/// <summary>
/// Write contents of a structure array.
/// </summary>
/// <param name="array">A structure array.</param>
/// <param name="name">Name of the array.</param>
public void WriteStructureArrayContents(IStructureArray array, string name)
{
WriteArrayFlags();
WriteDimensions(array.Dimensions);
WriteName(name);
WriteFieldNames(array.FieldNames);
WriteStructureArrayValues(array);
}
/// <summary>
/// Write contents of a cell array.
/// </summary>
/// <param name="array">A cell array.</param>
/// <param name="name">Name of the array.</param>
public void WriteCellArrayContents(ICellArray array, string name)
{
WriteArrayFlags();
WriteDimensions(array.Dimensions);
WriteName(name);
WriteCellArrayValues(array);
}
private void WriteTag()
{
Position += 8;
}
private void WriteShortTag()
{
Position += 4;
}
private void WriteWrappingContents<T>(T array, Action<FakeWriter> writeContents)
where T : IArray
{
if (array.IsEmpty)
{
WriteTag();
return;
}
WriteTag();
writeContents(this);
}
private void WriteNumericalArrayValues(IArray value)
{
switch (value)
{
case IArrayOf<sbyte> sbyteArray:
WriteDataElement(GetLengthOfByteArray<sbyte>(sbyteArray.Data.Length));
break;
case IArrayOf<byte> byteArray:
WriteDataElement(GetLengthOfByteArray<byte>(byteArray.Data.Length));
break;
case IArrayOf<short> shortArray:
WriteDataElement(GetLengthOfByteArray<short>(shortArray.Data.Length));
break;
case IArrayOf<ushort> ushortArray:
WriteDataElement(GetLengthOfByteArray<ushort>(ushortArray.Data.Length));
break;
case IArrayOf<int> intArray:
WriteDataElement(GetLengthOfByteArray<int>(intArray.Data.Length));
break;
case IArrayOf<uint> uintArray:
WriteDataElement(GetLengthOfByteArray<uint>(uintArray.Data.Length));
break;
case IArrayOf<long> longArray:
WriteDataElement(GetLengthOfByteArray<long>(longArray.Data.Length));
break;
case IArrayOf<ulong> ulongArray:
WriteDataElement(GetLengthOfByteArray<ulong>(ulongArray.Data.Length));
break;
case IArrayOf<float> floatArray:
WriteDataElement(GetLengthOfByteArray<float>(floatArray.Data.Length));
break;
case IArrayOf<double> doubleArray:
WriteDataElement(GetLengthOfByteArray<double>(doubleArray.Data.Length));
break;
case IArrayOf<bool> boolArray:
WriteDataElement(boolArray.Data.Length);
break;
case IArrayOf<ComplexOf<sbyte>> complexSbyteArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexSbyteArray.Data));
break;
case IArrayOf<ComplexOf<byte>> complexByteArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexByteArray.Data));
break;
case IArrayOf<ComplexOf<short>> complexShortArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexShortArray.Data));
break;
case IArrayOf<ComplexOf<ushort>> complexUshortArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexUshortArray.Data));
break;
case IArrayOf<ComplexOf<int>> complexIntArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexIntArray.Data));
break;
case IArrayOf<ComplexOf<uint>> complexUintArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexUintArray.Data));
break;
case IArrayOf<ComplexOf<long>> complexLongArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexLongArray.Data));
break;
case IArrayOf<ComplexOf<ulong>> complexUlongArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexUlongArray.Data));
break;
case IArrayOf<ComplexOf<float>> complexFloatArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexFloatArray.Data));
break;
case IArrayOf<Complex> complexDoubleArray:
WriteComplexValues(GetLengthOfPairOfByteArrays(complexDoubleArray.Data));
break;
default:
throw new NotSupportedException();
}
}
private void WriteName(string name)
{
var nameBytes = Encoding.ASCII.GetBytes(name);
WriteDataElement(nameBytes.Length);
}
private void WriteArrayFlags()
{
WriteTag();
Position += 8;
}
private void WriteDimensions(int[] dimensions)
{
var buffer = GetLengthOfByteArray<int>(dimensions.Length);
WriteDataElement(buffer);
}
private static unsafe int GetLengthOfByteArray<T>(int dataLength)
where T : unmanaged
{
return dataLength * sizeof(T);
}
private static unsafe int GetLengthOfPairOfByteArrays<T>(ComplexOf<T>[] data)
where T : unmanaged
{
return data.Length * sizeof(T);
}
private static unsafe int GetLengthOfPairOfByteArrays(Complex[] data)
{
return data.Length * sizeof(double);
}
private static int CalculatePadding(int length)
{
var rem = length % 8;
return rem == 0 ? 0 : 8 - rem;
}
private void WriteDataElement(int dataLength)
{
var maybePadding = 0;
if (dataLength > 4)
{
WriteTag();
Position += dataLength;
maybePadding = CalculatePadding(dataLength + 8);
}
else
{
WriteShortTag();
Position += 4;
}
Position += maybePadding;
}
private void WriteComplexValues(
int dataLength)
{
WriteDataElement(dataLength);
WriteDataElement(dataLength);
}
private void WriteSparseArrayValues<T>(int rowsLength, int columnsLength, int dataLength)
where T : unmanaged
{
WriteDataElement(GetLengthOfByteArray<int>(rowsLength));
WriteDataElement(GetLengthOfByteArray<int>(columnsLength));
if (typeof(T) == typeof(double))
{
WriteDataElement(GetLengthOfByteArray<double>(dataLength));
}
else if (typeof(T) == typeof(Complex))
{
WriteDataElement(GetLengthOfByteArray<double>(dataLength));
WriteDataElement(GetLengthOfByteArray<double>(dataLength));
}
else if (typeof(T) == typeof(bool))
{
WriteDataElement(dataLength);
}
}
private static (int rowIndexLength, int columnIndexLength, int dataLength, uint nonZero) PrepareSparseArrayData<T>(
ISparseArrayOf<T> array)
where T : struct, IEquatable<T>
{
var numberOfColumns = array.Dimensions[1];
var numberOfElements = array.Data.Values.Count(value => !value.Equals(default));
return (numberOfElements, numberOfColumns + 1, numberOfElements, (uint)numberOfElements);
}
private void WriteSparseArrayFlags()
{
WriteTag();
Position += 8;
}
private void WriteFieldNames(IEnumerable<string> fieldNames)
{
var fieldNamesArray = fieldNames.Select(name => Encoding.ASCII.GetBytes(name)).ToArray();
var maxFieldName = fieldNamesArray.Max(name => name.Length) + 1;
WriteDataElement(GetLengthOfByteArray<int>(1));
var buffer = new byte[fieldNamesArray.Length * maxFieldName];
var startPosition = 0;
foreach (var name in fieldNamesArray)
{
for (var i = 0; i < name.Length; i++)
{
buffer[startPosition + i] = name[i];
}
startPosition += maxFieldName;
}
WriteDataElement(buffer.Length);
}
private void WriteStructureArrayValues(IStructureArray array)
{
for (var i = 0; i < array.Count; i++)
{
foreach (var name in array.FieldNames)
{
WriteArray(array[name, i]);
}
}
}
private void WriteArray(IArray array, string variableName = "", bool isGlobal = false)
{
switch (array)
{
case ICharArray charArray:
WriteCharArray(charArray, variableName);
break;
case ISparseArrayOf<double> doubleSparseArray:
WriteSparseArray(doubleSparseArray, variableName);
break;
case ISparseArrayOf<Complex> complexSparseArray:
WriteSparseArray(complexSparseArray, variableName);
break;
case ISparseArrayOf<bool> boolSparseArray:
WriteSparseArray(boolSparseArray, variableName);
break;
case ICellArray cellArray:
WriteCellArray(cellArray, variableName);
break;
case IStructureArray structureArray:
WriteStructureArray(structureArray, variableName);
break;
default:
WriteNumericalArray(array, variableName);
break;
}
}
private void WriteCharArray(ICharArray charArray, string name)
{
WriteWrappingContents(
charArray,
fakeWriter => fakeWriter.WriteCharArrayContents(charArray, name));
}
private void WriteSparseArray<T>(ISparseArrayOf<T> sparseArray, string name)
where T : unmanaged, IEquatable<T>
{
WriteWrappingContents(
sparseArray,
fakeWriter => fakeWriter.WriteSparseArrayContents(sparseArray, name));
}
private void WriteCellArray(ICellArray cellArray, string name)
{
WriteWrappingContents(
cellArray,
fakeWriter => fakeWriter.WriteCellArrayContents(cellArray, name));
}
private void WriteCellArrayValues(ICellArray array)
{
for (var i = 0; i < array.Count; i++)
{
WriteArray(array[i]);
}
}
private void WriteStructureArray(
IStructureArray structureArray,
string name)
{
WriteWrappingContents(
structureArray,
fakeWriter => fakeWriter.WriteStructureArrayContents(structureArray, name));
}
private void WriteNumericalArray(
IArray numericalArray,
string name = "")
{
WriteWrappingContents(
numericalArray,
fakeWriter => fakeWriter.WriteNumericalArrayContents(numericalArray, name));
}
}
}

View File

@ -1,11 +1,9 @@
// Copyright 2017 Alexander Luzgarev
using System;
namespace MatFileHandler
{
/// <summary>
/// Exception related to Matlab data handling
/// Exception related to Matlab data handling.
/// </summary>
public class HandlerException : Exception
{
@ -14,9 +12,9 @@ namespace MatFileHandler
/// </summary>
/// <param name="message">Error message.</param>
/// <param name="innerException">Inner exception.</param>
public HandlerException(string message, Exception innerException = null)
public HandlerException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
}
}

View File

@ -1,10 +1,10 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Globalization;
using System.IO;
using System.Linq;
#if !NET461
using System.Runtime.InteropServices;
#endif
namespace MatFileHandler
{
@ -13,7 +13,7 @@ namespace MatFileHandler
/// </summary>
internal class Header
{
private Header(string text, byte[] subsystemDataOffset, int version)
private Header(string text, long subsystemDataOffset, int version)
{
Text = text;
SubsystemDataOffset = subsystemDataOffset;
@ -28,7 +28,7 @@ namespace MatFileHandler
/// <summary>
/// Gets subsystem data offset.
/// </summary>
public byte[] SubsystemDataOffset { get; }
public long SubsystemDataOffset { get; }
/// <summary>
/// Gets file version.
@ -55,7 +55,7 @@ namespace MatFileHandler
platform = platform.Remove(length);
}
var text = $"MATLAB 5.0 MAT-file, Platform: {platform}, Created on: {dateTime}{padding}";
return new Header(text, subsystemDataOffset, 256);
return new Header(text, 0, 256);
}
/// <summary>
@ -67,15 +67,14 @@ namespace MatFileHandler
{
var textBytes = reader.ReadBytes(116);
var text = System.Text.Encoding.UTF8.GetString(textBytes);
var subsystemDataOffset = reader.ReadBytes(8);
var subsystemDataOffsetBytes = reader.ReadBytes(8);
var subsystemDataOffset = BitConverter.ToInt64(subsystemDataOffsetBytes, 0);
var version = reader.ReadInt16();
var endian = reader.ReadInt16();
var isLittleEndian = endian == 19785;
if (!isLittleEndian)
{
throw new NotSupportedException("Big-endian files are not supported.");
}
return new Header(text, subsystemDataOffset, version);
return isLittleEndian
? new Header(text, subsystemDataOffset, version)
: throw new NotSupportedException("Big-endian files are not supported.");
}
private static string GetOperatingSystem()
@ -99,4 +98,4 @@ namespace MatFileHandler
#endif
}
}
}
}

View File

@ -1,5 +1,4 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Numerics;
namespace MatFileHandler
@ -28,12 +27,24 @@ namespace MatFileHandler
/// Tries to convert the array to an array of Double values.
/// </summary>
/// <returns>Array of values of the array, converted to Double, or null if the conversion is not possible.</returns>
double[] ConvertToDoubleArray();
double[]? ConvertToDoubleArray();
/// <summary>
/// Tries to convert the array to a 2-dimensional array of Double values.
/// </summary>
/// <returns>2-dimensional array of values of the array, converted to Double, or null if the conversion is not possible (for example, when the array has more than 2 dimensions).</returns>
double[,]? ConvertTo2dDoubleArray();
/// <summary>
/// Tries to convert the array to a multidimensional array of Double values.
/// </summary>
/// <returns>Multidimensional array of values of the array, converted to Double, or null if the conversion is not possible.</returns>
Array? ConvertToMultidimensionalDoubleArray();
/// <summary>
/// Tries to convert the array to an array of Complex values.
/// </summary>
/// <returns>Array of values of the array, converted to Complex, or null if the conversion is not possible.</returns>
Complex[] ConvertToComplexArray();
Complex[]? ConvertToComplexArray();
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -21,7 +19,7 @@ namespace MatFileHandler
/// * for cell arrays:
/// IArray;
/// * for structure arrays:
/// IReadOnlyDictionary&lt;string, IArray&gt;;
/// IReadOnlyDictionary&lt;string, IArray&gt;.
/// </remarks>
public interface IArrayOf<T> : IArray
{
@ -36,4 +34,4 @@ namespace MatFileHandler
/// <param name="list">Index of the element.</param>
T this[params int[] list] { get; set; }
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -8,4 +6,4 @@ namespace MatFileHandler
public interface ICellArray : IArrayOf<IArray>
{
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -10,6 +8,8 @@ namespace MatFileHandler
/// <summary>
/// Gets the contained string.
/// </summary>
#pragma warning disable CA1716, CA1720
string String { get; }
#pragma warning restore CA1716, CA1720
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -20,5 +18,13 @@ namespace MatFileHandler
/// </summary>
/// <param name="variableName">Variable name.</param>
IVariable this[string variableName] { get; set; }
/// <summary>
/// Gets the variable with the specified name.
/// </summary>
/// <param name="name">The name of the variable to get.</param>
/// <param name="variable">When this method returns, contains the variable with the specified name, if it is found; otherwise, null.</param>
/// <returns>True if the file contains a variable with the specified name; otherwise, false.</returns>
bool TryGetVariable(string name, out IVariable? variable);
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace MatFileHandler
{
/// <summary>
/// An interface to access Matlab objects (more precisely, "object arrays").
/// This is very similar to the <see cref="IStructureArray"/> interface:
/// an object holds fields that you can access, and the name of its class.
/// Additionally, you can treat is as an array of dictionaries mapping
/// field names to contents of fields.
/// </summary>
public interface IMatObject : IArrayOf<IReadOnlyDictionary<string, IArray>>
{
/// <summary>
/// Gets the name of object's class.
/// </summary>
string ClassName { get; }
/// <summary>
/// Gets the names of object's fields.
/// </summary>
IEnumerable<string> FieldNames { get; }
/// <summary>
/// Access a given field of a given object in the array.
/// </summary>
/// <param name="field">Field name.</param>
/// <param name="list">Index of the object to access.</param>
/// <returns>The value of the field in the selected object.</returns>
IArray this[string field, params int[] list] { get; set; }
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System.Collections.Generic;
namespace MatFileHandler
@ -15,6 +13,6 @@ namespace MatFileHandler
/// <summary>
/// Gets a dictionary mapping indices to values.
/// </summary>
new IReadOnlyDictionary<(int, int), T> Data { get; }
new IReadOnlyDictionary<(int row, int column), T> Data { get; }
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System.Collections.Generic;
namespace MatFileHandler
@ -21,4 +19,4 @@ namespace MatFileHandler
/// <param name="list">Index of the element in the structure array.</param>
IArray this[string field, params int[] list] { get; set; }
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -22,4 +20,4 @@ namespace MatFileHandler
/// </summary>
bool IsGlobal { get; }
}
}
}

View File

@ -1,5 +1,4 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Numerics;
namespace MatFileHandler
@ -50,19 +49,31 @@ namespace MatFileHandler
/// <returns>Empty array.</returns>
public static MatArray Empty()
{
return new MatArray(new ArrayFlags { Class = ArrayType.MxCell, Variable = 0 }, new int[] { }, string.Empty);
return new MatArray(new ArrayFlags { Class = ArrayType.MxCell, Variable = 0 }, Array.Empty<int>(), string.Empty);
}
/// <inheritdoc />
public virtual double[] ConvertToDoubleArray()
public virtual double[]? ConvertToDoubleArray()
{
return null;
}
/// <inheritdoc />
public virtual Complex[] ConvertToComplexArray()
public virtual double[,]? ConvertTo2dDoubleArray()
{
return ConvertToMultidimensionalDoubleArray() as double[,];
}
/// <inheritdoc />
public virtual Array? ConvertToMultidimensionalDoubleArray()
{
return null;
}
/// <inheritdoc />
public virtual Complex[]? ConvertToComplexArray()
{
return null;
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System.Collections.Generic;
using System.Linq;
@ -33,4 +31,4 @@ namespace MatFileHandler
set => Data[Dimensions.DimFlatten(indices)] = value;
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -54,4 +52,4 @@ namespace MatFileHandler
}
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System.Collections.Generic;
using System.Linq;
@ -18,11 +16,7 @@ namespace MatFileHandler
/// <param name="variables">List of variables.</param>
public MatFile(IEnumerable<IVariable> variables)
{
_variables = new Dictionary<string, IVariable>();
foreach (var variable in variables)
{
_variables[variable.Name] = variable;
}
_variables = variables.ToDictionary(v => v.Name, v => v);
}
/// <inheritdoc />
@ -34,5 +28,11 @@ namespace MatFileHandler
get => _variables[variableName];
set => _variables[variableName] = value;
}
/// <inheritdoc />
public bool TryGetVariable(string name, out IVariable value)
{
return _variables.TryGetValue(name, out value);
}
}
}
}

View File

@ -1,34 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<PackageVersion>1.1.0</PackageVersion>
<TargetFrameworks>netstandard2.0;net461;net472</TargetFrameworks>
<PackageVersion>1.4.0-beta6</PackageVersion>
<PackageId>MatFileHandler</PackageId>
<Title>A library for reading and writing MATLAB .mat files.</Title>
<Authors>Alexander Luzgarev</Authors>
<Description>MatFileHandler provides a simple interface for reading and writing MATLAB .mat files (of so-called "Level 5") and extracting the contents of numerical arrays, logical arrays, sparse arrays, char arrays, cell arrays and structure arrays.</Description>
<Copyright>Copyright 2017-2018 Alexander Luzgarev</Copyright>
<PackageLicenseUrl>https://raw.githubusercontent.com/mahalex/MatFileHandler/master/LICENSE.md</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/mahalex/MatFileHandler</PackageProjectUrl>
<PackageReleaseNotes>First release.</PackageReleaseNotes>
<Copyright>Copyright 2017-2020 Alexander Luzgarev</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://git.mahalex.net/mahalex/MatFileHandler</PackageProjectUrl>
<PackageReleaseNotes>https://git.mahalex.net/mahalex/MatFileHandler/releases/</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>Matlab</PackageTags>
<RepositoryUrl>https://github.com/mahalex/MatFileHandler</RepositoryUrl>
<RepositoryUrl>https://git.mahalex.net/mahalex/MatFileHandler</RepositoryUrl>
<OutputPath>bin\$(Configuration)\</OutputPath>
<DocumentationFile>$(OutputPath)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<CodeAnalysisRuleSet>..\MatFileHandler.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<CodeAnalysisRuleSet>..\MatFileHandler.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.4.0" Condition="'$(TargetFramework)' == 'net461'" />
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Gitea" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\"/>
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="MatFileHandler.Tests" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System.Collections.Generic;
using System.IO;
@ -27,41 +25,90 @@ namespace MatFileHandler
/// <returns>Contents of the file.</returns>
public IMatFile Read()
{
using (var reader = new BinaryReader(Stream))
{
return Read(reader);
}
using var reader = new BinaryReader(new PositionTrackingStream(Stream));
return Read(reader);
}
private static void ReadHeader(BinaryReader reader)
/// <summary>
/// Read a sequence of raw variables from .mat file.
/// </summary>
/// <param name="reader">Reader.</param>
/// <param name="subsystemDataOffset">Offset of subsystem data in the file;
/// we need it because we may encounter it during reading, and
/// the subsystem data should be parsed in a special way.</param>
/// <param name="subsystemData">
/// Link to the current file's subsystem data structure; initially it has dummy value
/// which will be replaced after we parse the whole subsystem data.</param>
/// <returns>List of "raw" variables; the actual variables are constructed from them later.</returns>
internal static List<RawVariable> ReadRawVariables(BinaryReader reader, long subsystemDataOffset, SubsystemData subsystemData)
{
Header.Read(reader);
}
private static IMatFile Read(BinaryReader reader)
{
ReadHeader(reader);
var variables = new List<IVariable>();
var variables = new List<RawVariable>();
var dataElementReader = new DataElementReader(subsystemData);
while (true)
{
try
{
var dataElement = DataElementReader.Read(reader) as MatArray;
if (dataElement == null)
{
continue;
}
variables.Add(new MatVariable(
dataElement,
dataElement.Name,
dataElement.Flags.Variable.HasFlag(Variable.IsGlobal)));
}
catch (EndOfStreamException)
var position = reader.BaseStream.Position;
var dataElement = dataElementReader.Read(reader);
if (dataElement is null)
{
break;
}
if (position == subsystemDataOffset)
{
var subsystemDataElement = dataElement as IArrayOf<byte>
?? throw new HandlerException("Cannot parse subsystem data element.");
var newSubsystemData = ReadSubsystemData(subsystemDataElement.Data, subsystemData);
subsystemData.Set(newSubsystemData);
}
else
{
variables.Add(new RawVariable(position, dataElement));
}
}
return variables;
}
/// <summary>
/// Read raw variables from a .mat file.
/// </summary>
/// <param name="reader">Binary reader.</param>
/// <param name="subsystemDataOffset">Offset to the subsystem data to use (read from the file header).</param>
/// <returns>Raw variables read.</returns>
internal static List<RawVariable> ReadRawVariables(BinaryReader reader, long subsystemDataOffset)
{
var subsystemData = new SubsystemData();
return ReadRawVariables(reader, subsystemDataOffset, subsystemData);
}
private static MatFile Read(BinaryReader reader)
{
var header = ReadHeader(reader);
var rawVariables = ReadRawVariables(reader, header.SubsystemDataOffset);
var variables = new List<IVariable>();
foreach (var variable in rawVariables)
{
if (variable.DataElement is MatArray array)
{
variables.Add(
new MatVariable(
array,
array.Name,
array.Flags.Variable.HasFlag(Variable.IsGlobal)));
}
}
return new MatFile(variables);
}
private static Header ReadHeader(BinaryReader reader)
{
return Header.Read(reader);
}
private static SubsystemData ReadSubsystemData(byte[] bytes, SubsystemData subsystemData)
{
return SubsystemDataReader.Read(bytes, subsystemData);
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.IO;
@ -15,13 +13,27 @@ namespace MatFileHandler
/// </summary>
public class MatFileWriter
{
private readonly MatFileWriterOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="MatFileWriter"/> class with a stream.
/// Initializes a new instance of the <see cref="MatFileWriter"/> class with a stream and default options.
/// </summary>
/// <param name="stream">Output stream.</param>
public MatFileWriter(Stream stream)
{
Stream = stream;
_options = MatFileWriterOptions.Default;
}
/// <summary>
/// Initializes a new instance of the <see cref="MatFileWriter"/> class with a stream.
/// </summary>
/// <param name="stream">Output stream.</param>
/// <param name="options">Options to use for file writing.</param>
public MatFileWriter(Stream stream, MatFileWriterOptions options)
{
Stream = stream;
_options = options;
}
private Stream Stream { get; }
@ -33,12 +45,28 @@ namespace MatFileHandler
public void Write(IMatFile file)
{
var header = Header.CreateNewHeader();
using (var writer = new BinaryWriter(Stream))
using var writer = new BinaryWriter(Stream);
WriteHeader(writer, header);
foreach (var variable in file.Variables)
{
WriteHeader(writer, header);
foreach (var variable in file.Variables)
switch (_options.UseCompression)
{
WriteCompressedVariable(writer, variable);
case CompressionUsage.Always:
if (Stream.CanSeek)
{
WriteCompressedVariableToSeekableStream(writer, variable);
}
else
{
WriteCompressedVariableToUnseekableStream(writer, variable);
}
break;
case CompressionUsage.Never:
WriteVariable(writer, variable);
break;
default:
throw new NotImplementedException();
}
}
}
@ -66,7 +94,7 @@ namespace MatFileHandler
return (s2 << 16) | s1;
}
private void WriteHeader(BinaryWriter writer, Header header)
private static void WriteHeader(BinaryWriter writer, Header header)
{
writer.Write(Encoding.UTF8.GetBytes(header.Text));
writer.Write(header.SubsystemDataOffset);
@ -74,33 +102,30 @@ namespace MatFileHandler
writer.Write((short)19785); // Magic number, 'IM'.
}
private void WriteTag(BinaryWriter writer, Tag tag)
private static void WriteTag(BinaryWriter writer, Tag tag)
{
writer.Write((int)tag.Type);
writer.Write(tag.Length);
}
private void WriteShortTag(BinaryWriter writer, Tag tag)
private static void WriteShortTag(BinaryWriter writer, Tag tag)
{
writer.Write((short)tag.Type);
writer.Write((short)tag.Length);
}
private void WritePadding(BinaryWriter writer)
{
var positionMod8 = writer.BaseStream.Position % 8;
if (positionMod8 != 0)
{
writer.Write(new byte[8 - positionMod8]);
}
}
private void WriteDataElement(BinaryWriter writer, DataType type, byte[] data)
private static void WriteDataElement(BinaryWriter writer, DataType type, byte[] data)
{
if (data.Length > 4)
{
WriteTag(writer, new Tag(type, data.Length));
writer.Write(data);
var rem = data.Length % 8;
if (rem > 0)
{
var padding = new byte[8 - rem];
writer.Write(padding);
}
}
else
{
@ -112,17 +137,16 @@ namespace MatFileHandler
writer.Write(padding);
}
}
WritePadding(writer);
}
private void WriteDimensions(BinaryWriter writer, int[] dimensions)
private static void WriteDimensions(BinaryWriter writer, int[] dimensions)
{
var buffer = ConvertToByteArray(dimensions);
WriteDataElement(writer, DataType.MiInt32, buffer);
}
private byte[] ConvertToByteArray<T>(T[] data)
where T : struct
private static byte[] ConvertToByteArray<T>(T[] data)
where T : struct
{
int size;
if (typeof(T) == typeof(sbyte))
@ -165,6 +189,10 @@ namespace MatFileHandler
{
size = sizeof(double);
}
else if (typeof(T) == typeof(bool))
{
size = sizeof(bool);
}
else
{
throw new NotSupportedException();
@ -174,51 +202,51 @@ namespace MatFileHandler
return buffer;
}
private (byte[], byte[]) ConvertToPairOfByteArrays<T>(ComplexOf<T>[] data)
private static (byte[] real, byte[] imaginary) ConvertToPairOfByteArrays<T>(ComplexOf<T>[] data)
where T : struct
{
return (ConvertToByteArray(data.Select(x => x.Real).ToArray()),
ConvertToByteArray(data.Select(x => x.Imaginary).ToArray()));
}
private (byte[], byte[]) ConvertToPairOfByteArrays(Complex[] data)
private static (byte[] real, byte[] imaginary) ConvertToPairOfByteArrays(Complex[] data)
{
return (ConvertToByteArray(data.Select(x => x.Real).ToArray()),
ConvertToByteArray(data.Select(x => x.Imaginary).ToArray()));
}
private void WriteComplexValues(BinaryWriter writer, DataType type, (byte[] real, byte[] complex) data)
private static void WriteComplexValues(BinaryWriter writer, DataType type, (byte[] real, byte[] complex) data)
{
WriteDataElement(writer, type, data.real);
WriteDataElement(writer, type, data.complex);
}
private void WriteArrayFlags(BinaryWriter writer, ArrayFlags flags)
private static void WriteArrayFlags(BinaryWriter writer, ArrayFlags flags)
{
var flag = (byte)flags.Variable;
WriteTag(writer, new Tag(DataType.MiUInt32, 8));
writer.Write((byte)flags.Class);
writer.Write(flag);
writer.Write(new byte[] { 0, 0, 0, 0, 0, 0 });
writer.Write([0, 0, 0, 0, 0, 0]);
}
private void WriteSparseArrayFlags(BinaryWriter writer, SparseArrayFlags flags)
private static void WriteSparseArrayFlags(BinaryWriter writer, SparseArrayFlags flags)
{
var flag = (byte)flags.ArrayFlags.Variable;
WriteTag(writer, new Tag(DataType.MiUInt32, 8));
writer.Write((byte)flags.ArrayFlags.Class);
writer.Write(flag);
writer.Write(new byte[] { 0, 0 });
writer.Write([0, 0]);
writer.Write(flags.NzMax);
}
private void WriteName(BinaryWriter writer, string name)
private static void WriteName(BinaryWriter writer, string name)
{
var nameBytes = Encoding.ASCII.GetBytes(name);
WriteDataElement(writer, DataType.MiInt8, nameBytes);
}
private void WriteNumericalArrayValues(BinaryWriter writer, IArray value)
private static void WriteNumericalArrayValues(BinaryWriter writer, IArray value)
{
switch (value)
{
@ -253,10 +281,7 @@ namespace MatFileHandler
WriteDataElement(writer, DataType.MiDouble, ConvertToByteArray(doubleArray.Data));
break;
case IArrayOf<bool> boolArray:
WriteDataElement(
writer,
DataType.MiUInt8,
boolArray.Data.Select(element => element ? (byte)1 : (byte)0).ToArray());
WriteDataElement(writer, DataType.MiUInt8, ConvertToByteArray(boolArray.Data));
break;
case IArrayOf<ComplexOf<sbyte>> complexSbyteArray:
WriteComplexValues(writer, DataType.MiInt8, ConvertToPairOfByteArrays(complexSbyteArray.Data));
@ -293,63 +318,39 @@ namespace MatFileHandler
}
}
private ArrayFlags GetArrayFlags(IArray array, bool isGlobal)
private static ArrayFlags GetArrayFlags(IArray array, bool isGlobal)
{
var variableFlags = isGlobal ? Variable.IsGlobal : 0;
switch (array)
return array switch
{
case IArrayOf<sbyte> _:
return new ArrayFlags(ArrayType.MxInt8, variableFlags);
case IArrayOf<byte> _:
return new ArrayFlags(ArrayType.MxUInt8, variableFlags);
case IArrayOf<short> _:
return new ArrayFlags(ArrayType.MxInt16, variableFlags);
case IArrayOf<ushort> _:
return new ArrayFlags(ArrayType.MxUInt16, variableFlags);
case IArrayOf<int> _:
return new ArrayFlags(ArrayType.MxInt32, variableFlags);
case IArrayOf<uint> _:
return new ArrayFlags(ArrayType.MxUInt32, variableFlags);
case IArrayOf<long> _:
return new ArrayFlags(ArrayType.MxInt64, variableFlags);
case IArrayOf<ulong> _:
return new ArrayFlags(ArrayType.MxUInt64, variableFlags);
case IArrayOf<float> _:
return new ArrayFlags(ArrayType.MxSingle, variableFlags);
case IArrayOf<double> _:
return new ArrayFlags(ArrayType.MxDouble, variableFlags);
case IArrayOf<bool> _:
return new ArrayFlags(ArrayType.MxUInt8, variableFlags | Variable.IsLogical);
case IArrayOf<ComplexOf<sbyte>> _:
return new ArrayFlags(ArrayType.MxInt8, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<byte>> _:
return new ArrayFlags(ArrayType.MxUInt8, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<short>> _:
return new ArrayFlags(ArrayType.MxInt16, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<ushort>> _:
return new ArrayFlags(ArrayType.MxUInt16, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<int>> _:
return new ArrayFlags(ArrayType.MxInt32, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<uint>> _:
return new ArrayFlags(ArrayType.MxUInt32, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<long>> _:
return new ArrayFlags(ArrayType.MxInt64, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<ulong>> _:
return new ArrayFlags(ArrayType.MxUInt64, variableFlags | Variable.IsComplex);
case IArrayOf<ComplexOf<float>> _:
return new ArrayFlags(ArrayType.MxSingle, variableFlags | Variable.IsComplex);
case IArrayOf<Complex> _:
return new ArrayFlags(ArrayType.MxDouble, variableFlags | Variable.IsComplex);
case IStructureArray _:
return new ArrayFlags(ArrayType.MxStruct, variableFlags);
case ICellArray _:
return new ArrayFlags(ArrayType.MxCell, variableFlags);
default:
throw new NotSupportedException();
}
IArrayOf<sbyte> => new ArrayFlags(ArrayType.MxInt8, variableFlags),
IArrayOf<byte> => new ArrayFlags(ArrayType.MxUInt8, variableFlags),
IArrayOf<short> => new ArrayFlags(ArrayType.MxInt16, variableFlags),
IArrayOf<ushort> => new ArrayFlags(ArrayType.MxUInt16, variableFlags),
IArrayOf<int> => new ArrayFlags(ArrayType.MxInt32, variableFlags),
IArrayOf<uint> => new ArrayFlags(ArrayType.MxUInt32, variableFlags),
IArrayOf<long> => new ArrayFlags(ArrayType.MxInt64, variableFlags),
IArrayOf<ulong> => new ArrayFlags(ArrayType.MxUInt64, variableFlags),
IArrayOf<float> => new ArrayFlags(ArrayType.MxSingle, variableFlags),
IArrayOf<double> => new ArrayFlags(ArrayType.MxDouble, variableFlags),
IArrayOf<bool> => new ArrayFlags(ArrayType.MxUInt8, variableFlags | Variable.IsLogical),
IArrayOf<ComplexOf<sbyte>> => new ArrayFlags(ArrayType.MxInt8, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<byte>> => new ArrayFlags(ArrayType.MxUInt8, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<short>> => new ArrayFlags(ArrayType.MxInt16, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<ushort>> => new ArrayFlags(ArrayType.MxUInt16, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<int>> => new ArrayFlags(ArrayType.MxInt32, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<uint>> => new ArrayFlags(ArrayType.MxUInt32, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<long>> => new ArrayFlags(ArrayType.MxInt64, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<ulong>> => new ArrayFlags(ArrayType.MxUInt64, variableFlags | Variable.IsComplex),
IArrayOf<ComplexOf<float>> => new ArrayFlags(ArrayType.MxSingle, variableFlags | Variable.IsComplex),
IArrayOf<Complex> => new ArrayFlags(ArrayType.MxDouble, variableFlags | Variable.IsComplex),
IStructureArray => new ArrayFlags(ArrayType.MxStruct, variableFlags),
ICellArray => new ArrayFlags(ArrayType.MxCell, variableFlags),
_ => throw new NotSupportedException(),
};
}
private SparseArrayFlags GetSparseArrayFlags<T>(ISparseArrayOf<T> array, bool isGlobal, uint nonZero)
private static SparseArrayFlags GetSparseArrayFlags<T>(ISparseArrayOf<T> array, bool isGlobal, uint nonZero)
where T : struct
{
var flags = GetArrayFlags(array, isGlobal);
@ -364,12 +365,16 @@ namespace MatFileHandler
};
}
private ArrayFlags GetCharArrayFlags(bool isGlobal)
private static ArrayFlags GetCharArrayFlags(bool isGlobal)
{
return new ArrayFlags(ArrayType.MxChar, isGlobal ? Variable.IsGlobal : 0);
}
private void WriteWrappingContents<T>(BinaryWriter writer, T array, Action<BinaryWriter> writeContents)
private static void WriteWrappingContents<T>(
BinaryWriter writer,
T array,
Action<FakeWriter> lengthCalculator,
Action<BinaryWriter> writeContents)
where T : IArray
{
if (array.IsEmpty)
@ -377,19 +382,15 @@ namespace MatFileHandler
WriteTag(writer, new Tag(DataType.MiMatrix, 0));
return;
}
using (var contents = new MemoryStream())
{
using (var contentsWriter = new BinaryWriter(contents))
{
writeContents(contentsWriter);
WriteTag(writer, new Tag(DataType.MiMatrix, (int)contents.Length));
contents.Position = 0;
contents.CopyTo(writer.BaseStream);
}
}
var fakeWriter = new FakeWriter();
lengthCalculator(fakeWriter);
var calculatedLength = fakeWriter.Position;
WriteTag(writer, new Tag(DataType.MiMatrix, calculatedLength));
writeContents(writer);
}
private void WriteNumericalArrayContents(BinaryWriter writer, IArray array, string name, bool isGlobal)
private static void WriteNumericalArrayContents(BinaryWriter writer, IArray array, string name, bool isGlobal)
{
WriteArrayFlags(writer, GetArrayFlags(array, isGlobal));
WriteDimensions(writer, array.Dimensions);
@ -397,7 +398,7 @@ namespace MatFileHandler
WriteNumericalArrayValues(writer, array);
}
private void WriteNumericalArray(
private static void WriteNumericalArray(
BinaryWriter writer,
IArray numericalArray,
string name = "",
@ -406,10 +407,11 @@ namespace MatFileHandler
WriteWrappingContents(
writer,
numericalArray,
fakeWriter => fakeWriter.WriteNumericalArrayContents(numericalArray, name),
contentsWriter => { WriteNumericalArrayContents(contentsWriter, numericalArray, name, isGlobal); });
}
private void WriteCharArrayContents(BinaryWriter writer, ICharArray charArray, string name, bool isGlobal)
private static void WriteCharArrayContents(BinaryWriter writer, ICharArray charArray, string name, bool isGlobal)
{
WriteArrayFlags(writer, GetCharArrayFlags(isGlobal));
WriteDimensions(writer, charArray.Dimensions);
@ -418,15 +420,16 @@ namespace MatFileHandler
WriteDataElement(writer, DataType.MiUtf16, ConvertToByteArray(array));
}
private void WriteCharArray(BinaryWriter writer, ICharArray charArray, string name, bool isGlobal)
private static void WriteCharArray(BinaryWriter writer, ICharArray charArray, string name, bool isGlobal)
{
WriteWrappingContents(
writer,
charArray,
fakeWriter => fakeWriter.WriteCharArrayContents(charArray, name),
contentsWriter => { WriteCharArrayContents(contentsWriter, charArray, name, isGlobal); });
}
private void WriteSparseArrayValues<T>(
private static void WriteSparseArrayValues<T>(
BinaryWriter writer, int[] rows, int[] columns, T[] data)
where T : struct
{
@ -436,9 +439,8 @@ namespace MatFileHandler
{
WriteDataElement(writer, DataType.MiDouble, ConvertToByteArray(data));
}
else if (data is Complex[])
else if (data is Complex[] complexData)
{
var complexData = data as Complex[];
WriteDataElement(
writer,
DataType.MiDouble,
@ -448,17 +450,16 @@ namespace MatFileHandler
DataType.MiDouble,
ConvertToByteArray(complexData.Select(c => c.Imaginary).ToArray()));
}
else if (data is bool[])
else if (data is bool[] boolData)
{
var boolData = data as bool[];
WriteDataElement(
writer,
DataType.MiUInt8,
boolData.Select(element => element ? (byte)1 : (byte)0).ToArray());
ConvertToByteArray(boolData));
}
}
private (int[] rowIndex, int[] columnIndex, T[] data, uint nonZero) PrepareSparseArrayData<T>(
private static (int[] rowIndex, int[] columnIndex, T[] data, uint nonZero) PrepareSparseArrayData<T>(
ISparseArrayOf<T> array)
where T : struct, IEquatable<T>
{
@ -472,8 +473,8 @@ namespace MatFileHandler
for (var column = 0; column < numberOfColumns; column++)
{
var column1 = column;
var thisColumn = keys.Where(pair => pair.Item2 == column1 && !dict[pair].Equals(default(T)));
var thisRow = thisColumn.Select(pair => pair.Item1).OrderBy(x => x).ToArray();
var thisColumn = keys.Where(pair => pair.column == column1 && !dict[pair].Equals(default));
var thisRow = thisColumn.Select(pair => pair.row).OrderBy(x => x).ToArray();
rowIndexList.AddRange(thisRow);
valuesList.AddRange(thisRow.Select(row => dict[(row, column1)]));
columnIndex[column + 1] = rowIndexList.Count;
@ -481,7 +482,7 @@ namespace MatFileHandler
return (rowIndexList.ToArray(), columnIndex, valuesList.ToArray(), (uint)rowIndexList.Count);
}
private void WriteSparseArrayContents<T>(
private static void WriteSparseArrayContents<T>(
BinaryWriter writer,
ISparseArrayOf<T> array,
string name,
@ -495,20 +496,21 @@ namespace MatFileHandler
WriteSparseArrayValues(writer, rows, columns, data);
}
private void WriteSparseArray<T>(BinaryWriter writer, ISparseArrayOf<T> sparseArray, string name, bool isGlobal)
where T : struct, IEquatable<T>
private static void WriteSparseArray<T>(BinaryWriter writer, ISparseArrayOf<T> sparseArray, string name, bool isGlobal)
where T : unmanaged, IEquatable<T>
{
WriteWrappingContents(
writer,
sparseArray,
fakeWriter => fakeWriter.WriteSparseArrayContents(sparseArray, name),
contentsWriter => { WriteSparseArrayContents(contentsWriter, sparseArray, name, isGlobal); });
}
private void WriteFieldNames(BinaryWriter writer, IEnumerable<string> fieldNames)
private static void WriteFieldNames(BinaryWriter writer, IEnumerable<string> fieldNames)
{
var fieldNamesArray = fieldNames.Select(name => Encoding.ASCII.GetBytes(name)).ToArray();
var maxFieldName = fieldNamesArray.Select(name => name.Length).Max() + 1;
WriteDataElement(writer, DataType.MiInt32, ConvertToByteArray(new[] { maxFieldName }));
var maxFieldName = fieldNamesArray.Max(name => name.Length) + 1;
WriteDataElement(writer, DataType.MiInt32, ConvertToByteArray([maxFieldName]));
var buffer = new byte[fieldNamesArray.Length * maxFieldName];
var startPosition = 0;
foreach (var name in fieldNamesArray)
@ -551,6 +553,7 @@ namespace MatFileHandler
WriteWrappingContents(
writer,
structureArray,
fakeWriter => fakeWriter.WriteStructureArrayContents(structureArray, name),
contentsWriter => { WriteStructureArrayContents(contentsWriter, structureArray, name, isGlobal); });
}
@ -575,6 +578,7 @@ namespace MatFileHandler
WriteWrappingContents(
writer,
cellArray,
fakeWriter => fakeWriter.WriteCellArrayContents(cellArray, name),
contentsWriter => { WriteCellArrayContents(contentsWriter, cellArray, name, isGlobal); });
}
@ -611,33 +615,55 @@ namespace MatFileHandler
WriteArray(writer, variable.Value, variable.Name, variable.IsGlobal);
}
private void WriteCompressedVariable(BinaryWriter writer, IVariable variable)
private void WriteCompressedVariableToSeekableStream(BinaryWriter writer, IVariable variable)
{
using (var compressedStream = new MemoryStream())
var position = writer.BaseStream.Position;
WriteTag(writer, new Tag(DataType.MiCompressed, 0));
writer.Write((byte)0x78);
writer.Write((byte)0x9c);
int compressedLength;
uint crc;
var before = writer.BaseStream.Position;
using (var compressionStream = new DeflateStream(writer.BaseStream, CompressionMode.Compress, leaveOpen: true))
{
uint crc;
using (var originalStream = new MemoryStream())
{
using (var internalWriter = new BinaryWriter(originalStream))
{
WriteVariable(internalWriter, variable);
originalStream.Position = 0;
crc = CalculateAdler32Checksum(originalStream);
originalStream.Position = 0;
using (var compressionStream =
new DeflateStream(compressedStream, CompressionMode.Compress, leaveOpen: true))
{
originalStream.CopyTo(compressionStream);
}
}
}
compressedStream.Position = 0;
WriteTag(writer, new Tag(DataType.MiCompressed, (int)(compressedStream.Length + 6)));
writer.Write((byte)0x78);
writer.Write((byte)0x9c);
compressedStream.CopyTo(writer.BaseStream);
writer.Write(BitConverter.GetBytes(crc).Reverse().ToArray());
using var checksumStream = new ChecksumCalculatingStream(compressionStream);
using var internalWriter = new BinaryWriter(checksumStream, Encoding.UTF8, leaveOpen: true);
WriteVariable(internalWriter, variable);
crc = checksumStream.GetCrc();
}
var after = writer.BaseStream.Position;
compressedLength = (int)(after - before) + 6;
writer.Write(BitConverter.GetBytes(crc).Reverse().ToArray());
writer.BaseStream.Position = position;
WriteTag(writer, new Tag(DataType.MiCompressed, compressedLength));
writer.BaseStream.Seek(0, SeekOrigin.End);
}
private void WriteCompressedVariableToUnseekableStream(BinaryWriter writer, IVariable variable)
{
using var compressedStream = new MemoryStream();
uint crc;
using (var originalStream = new MemoryStream())
{
using var internalWriter = new BinaryWriter(originalStream);
WriteVariable(internalWriter, variable);
originalStream.Position = 0;
crc = CalculateAdler32Checksum(originalStream);
originalStream.Position = 0;
using var compressionStream = new DeflateStream(
compressedStream,
CompressionMode.Compress,
leaveOpen: true);
originalStream.CopyTo(compressionStream);
}
compressedStream.Position = 0;
WriteTag(writer, new Tag(DataType.MiCompressed, (int)(compressedStream.Length + 6)));
writer.Write((byte)0x78);
writer.Write((byte)0x9c);
compressedStream.CopyTo(writer.BaseStream);
writer.Write(BitConverter.GetBytes(crc).Reverse().ToArray());
}
}
}
}

View File

@ -0,0 +1,21 @@
namespace MatFileHandler
{
/// <summary>
/// Options for writing .mat files.
/// </summary>
public struct MatFileWriterOptions
{
/// <summary>
/// Gets default options.
/// </summary>
public static MatFileWriterOptions Default => new()
{
UseCompression = CompressionUsage.Always,
};
/// <summary>
/// Gets or sets a value indicating whether to compress all variables when writing the file.
/// </summary>
public CompressionUsage UseCompression { get; set; }
}
}

View File

@ -1,8 +1,4 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace MatFileHandler
@ -43,58 +39,160 @@ namespace MatFileHandler
/// <returns>Array of values of the array, converted to Double, or null if the conversion is not possible.</returns>
public override double[] ConvertToDoubleArray()
{
return Data as double[] ?? Data.Select(x => Convert.ToDouble(x)).ToArray();
return Data switch
{
sbyte[] sbyteData => DataExtraction.SbyteToDouble(sbyteData),
byte[] byteData => DataExtraction.ByteToDouble(byteData),
short[] shortData => DataExtraction.ShortToDouble(shortData),
ushort[] ushortData => DataExtraction.UshortToDouble(ushortData),
int[] intData => DataExtraction.IntToDouble(intData),
uint[] uintData => DataExtraction.UintToDouble(uintData),
long[] longData => DataExtraction.LongToDouble(longData),
ulong[] ulongData => DataExtraction.UlongToDouble(ulongData),
float[] floatData => DataExtraction.FloatToDouble(floatData),
double[] doubleData => doubleData,
_ => throw new HandlerException("Cannot convert data to double array.")
};
}
/// <inheritdoc />
public override Array? ConvertToMultidimensionalDoubleArray()
{
var doubleData = ConvertToDoubleArray();
var rearrangedData = Dimensions.UnflattenArray(doubleData);
return rearrangedData;
}
/// <summary>
/// Tries to convert the array to an array of Complex values.
/// </summary>
/// <returns>Array of values of the array, converted to Complex, or null if the conversion is not possible.</returns>
public override Complex[] ConvertToComplexArray()
public override Complex[]? ConvertToComplexArray()
{
if (Data is Complex[])
return Data switch
{
return Data as Complex[];
}
if (Data is ComplexOf<sbyte>[])
{
return ConvertToComplex(Data as ComplexOf<sbyte>[]);
}
if (Data is ComplexOf<byte>[])
{
return ConvertToComplex(Data as ComplexOf<byte>[]);
}
if (Data is ComplexOf<short>[])
{
return ConvertToComplex(Data as ComplexOf<short>[]);
}
if (Data is ComplexOf<ushort>[])
{
return ConvertToComplex(Data as ComplexOf<ushort>[]);
}
if (Data is ComplexOf<int>[])
{
return ConvertToComplex(Data as ComplexOf<int>[]);
}
if (Data is ComplexOf<uint>[])
{
return ConvertToComplex(Data as ComplexOf<uint>[]);
}
if (Data is ComplexOf<long>[])
{
return ConvertToComplex(Data as ComplexOf<long>[]);
}
if (Data is ComplexOf<ulong>[])
{
return ConvertToComplex(Data as ComplexOf<ulong>[]);
}
return ConvertToDoubleArray().Select(x => new Complex(x, 0.0)).ToArray();
Complex[] data => data,
ComplexOf<sbyte>[] ofs => ConvertToComplex(ofs),
ComplexOf<byte>[] ofs => ConvertToComplex(ofs),
ComplexOf<short>[] ofs => ConvertToComplex(ofs),
ComplexOf<ushort>[] ofs => ConvertToComplex(ofs),
ComplexOf<int>[] ofs => ConvertToComplex(ofs),
ComplexOf<uint>[] ofs => ConvertToComplex(ofs),
ComplexOf<long>[] ofs => ConvertToComplex(ofs),
ComplexOf<ulong>[] ofs => ConvertToComplex(ofs),
ComplexOf<float>[] ofs => ConvertToComplex(ofs),
_ => ConvertToComplex(ConvertToDoubleArray())
};
}
private static Complex[] ConvertToComplex<TS>(IEnumerable<ComplexOf<TS>> array)
where TS : struct
private static Complex[] ConvertToComplex(ComplexOf<sbyte>[] array)
{
return array.Select(x => new Complex(Convert.ToDouble(x.Real), Convert.ToDouble(x.Imaginary))).ToArray();
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<byte>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<short>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<ushort>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<int>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<uint>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<long>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<ulong>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(ComplexOf<float>[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(Convert.ToDouble(array[i].Real), Convert.ToDouble(array[i].Imaginary));
}
return result;
}
private static Complex[] ConvertToComplex(double[] array)
{
var result = new Complex[array.Length];
for (var i = 0; i < array.Length; i++)
{
result[i] = new Complex(array[i], 0.0);
}
return result;
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections.Generic;
using System.Linq;
@ -26,7 +24,7 @@ namespace MatFileHandler
SparseArrayFlags flags,
int[] dimensions,
string name,
Dictionary<(int, int), T> data)
Dictionary<(int row, int column), T> data)
: base(flags.ArrayFlags, dimensions, name)
{
DataDictionary = data;
@ -39,9 +37,9 @@ namespace MatFileHandler
.ToArray();
/// <inheritdoc />
public IReadOnlyDictionary<(int, int), T> Data => DataDictionary;
public IReadOnlyDictionary<(int row, int column), T> Data => DataDictionary;
private Dictionary<(int, int), T> DataDictionary { get; }
private Dictionary<(int row, int column), T> DataDictionary { get; }
/// <inheritdoc />
public T this[params int[] list]
@ -49,7 +47,7 @@ namespace MatFileHandler
get
{
var rowAndColumn = GetRowAndColumn(list);
return DataDictionary.ContainsKey(rowAndColumn) ? DataDictionary[rowAndColumn] : default(T);
return DataDictionary.TryGetValue(rowAndColumn, out var result) ? result : default;
}
set => DataDictionary[GetRowAndColumn(list)] = value;
}
@ -61,7 +59,24 @@ namespace MatFileHandler
public override double[] ConvertToDoubleArray()
{
var data = ((IArrayOf<T>)this).Data;
return data as double[] ?? data.Select(x => Convert.ToDouble(x)).ToArray();
return data as double[] ?? data.Select(x => Convert.ToDouble(x, System.Globalization.CultureInfo.InvariantCulture)).ToArray();
}
/// <inheritdoc />
public override Array? ConvertToMultidimensionalDoubleArray()
{
if (Dimensions.Length != 2)
{
return null;
}
var result = new double[Dimensions[0], Dimensions[1]];
foreach (var pair in Data)
{
result[pair.Key.row, pair.Key.column] = Convert.ToDouble(pair.Value, System.Globalization.CultureInfo.InvariantCulture);
}
return result;
}
/// <summary>
@ -76,15 +91,12 @@ namespace MatFileHandler
private (int row, int column) GetRowAndColumn(int[] indices)
{
switch (indices.Length)
return indices.Length switch
{
case 1:
return (indices[0] % Dimensions[0], indices[0] / Dimensions[0]);
case 2:
return (indices[0], indices[1]);
default:
throw new NotSupportedException("Invalid index for sparse array.");
}
1 => (indices[0] % Dimensions[0], indices[0] / Dimensions[0]),
2 => (indices[0], indices[1]),
_ => throw new NotSupportedException("Invalid index for sparse array."),
};
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
using System;
using System.Collections;
using System.Collections.Generic;
@ -35,7 +33,19 @@ namespace MatFileHandler
/// <summary>
/// Gets null: not implemented.
/// </summary>
public IReadOnlyDictionary<string, IArray>[] Data => null;
public IReadOnlyDictionary<string, IArray>[] Data
{
get
{
var result = new IReadOnlyDictionary<string, IArray>[Count];
for (var i = 0; i < Count; i++)
{
result[i] = FieldNames.ToDictionary(name => name, name => Fields[name][i]);
}
return result;
}
}
/// <summary>
/// Gets a dictionary that maps field names to lists of values.
@ -57,7 +67,7 @@ namespace MatFileHandler
"Cannot set structure elements via this[params int[]] indexer. Use this[string, int[]] instead.");
}
private IReadOnlyDictionary<string, IArray> ExtractStructure(int i)
private MatStructureArrayElement ExtractStructure(int i)
{
return new MatStructureArrayElement(this, i);
}
@ -128,7 +138,7 @@ namespace MatFileHandler
/// <summary>
/// Checks if the structure has a given field.
/// </summary>
/// <param name="key">Field name</param>
/// <param name="key">Field name.</param>
/// <returns>True iff the structure has a given field.</returns>
public bool ContainsKey(string key) => Parent.Fields.ContainsKey(key);
@ -138,12 +148,14 @@ namespace MatFileHandler
/// <param name="key">Field name.</param>
/// <param name="value">Value (or null if the field is not present).</param>
/// <returns>Success status of the query.</returns>
public bool TryGetValue(string key, out IArray value)
#pragma warning disable CS8767
public bool TryGetValue(string key, out IArray? value)
#pragma warning restore CS8767
{
var success = Parent.Fields.TryGetValue(key, out var array);
if (!success)
{
value = default(IArray);
value = default;
return false;
}
value = array[Index];

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <inheritdoc />
@ -27,4 +25,4 @@ namespace MatFileHandler
/// <inheritdoc />
public bool IsGlobal { get; }
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -23,4 +21,4 @@ namespace MatFileHandler
/// </summary>
public T[] Data { get; }
}
}
}

View File

@ -0,0 +1,27 @@
namespace MatFileHandler;
/// <summary>
/// Parser for object data.
/// </summary>
internal static class ObjectParser
{
/// <summary>
/// Parse object data.
/// </summary>
/// <param name="uintArray">Opaque link array.</param>
/// <param name="subsystemData">Current subsystem data.</param>
/// <returns>Parsed object.</returns>
public static IArray ParseObject(MatNumericalArrayOf<uint> uintArray, SubsystemData subsystemData)
{
var (dimensions, indexToObjectId, classIndex) = DataElementReader.ParseOpaqueData(uintArray.Data);
return new OpaqueLink(
uintArray.Name,
string.Empty,
string.Empty,
dimensions,
uintArray,
indexToObjectId,
classIndex,
subsystemData);
}
}

201
MatFileHandler/Objects.md Normal file
View File

@ -0,0 +1,201 @@
# MATLAB objects
This document describes the format that MATLAB uses to store (new-style)
objects in .mat files. As far as I know, this format is not documented
anywhere. The [official MATLAB documentation](https://www.mathworks.com/help/pdf_doc/matlab/matfile_format.pdf)
(as of version 2018) does not even mention the corresponding MX class.
We assume the reader's familiarity with the document linked above.
It describes how the "normal" MATLAB data types (matrices, cell arrays,
structure arrays, etc.) are stored in .mat files. Objects (instances
of user-defined classes, or of build-in MATLAB types such as tables, etc.)
are stored in a slightly different way. If you save a variable into a .mat
file, it appears alongside the "normal" variables, but its data only contains
a "reference" that points to the actual object contents.
The actual data are stored in a so called "subsystem data" in a separate
portion of a .mat file.
Recall that .mat file header contains (among some text information and
a couple of flags) a "subsystem data offset field".
As described in the official documentation, it appears right after
the first 124 bytes of a .mat file (which contain a "header text field")
and occupies 16 bit, i.e., is a short (16-bit) integer.
This integer in fact tells you the offset in the .mat file
(starting from the very beginning of the file) where the subsystem data
starts. Typically it appears after all the "normal" variables.
So, we have to describe two things: how the subsystem data is organized and how
the references to it are stored.
Let's start with the second. Every MATLAB matrix is stored in a data element
of type `miMATRIX = 14`. The type of its elements is determined by
a "MATLAB array type" field (see table on page 1-16):
`mxCELL_CLASS = 1` means cell array, `mxSTRUCT_CLASS = 2` means structure
array, and so on, up to `mxUINT64_CLASS = 15` (unsigned 64-bit integer).
The official table ends here, but in fact there are (at least) to more
classes: `mxFUNCTION_CLASS = 16` for function handles,
and `mxOPAQUE_CLASS = 17` for "opaque objects", and that is what we are
interested in.
## Opaque data element format
After the array flags element that has array class field set
to `mxOPAQUE_CLASS = 17`, come four more data elements:
* Array name. This is the variable name in case of variables, and empty
in other cases (same as for "normal" data types).
* Type system name. This is stored in the same format as array name
(class `miINT8`), and contains four characters: `MCOS`. It stands for
"MATLAB Class Object System".
* Class name. Again, the same format for storing strings. This element
contains name of the class that this object belongs to.
* Data reference. This element is a matrix of class `mxUINT32`
and has size *n*×1, where *n* is at least 6.
The 32-bit unsigned integers inside contain information that you need to find
the actual object's data inside subsystem data:
* First number is 3707764736 (0xdd000000). This is the "magic" number
that is used to identify MATLAB object data. We'll see it used
(quite unexpectedly) below.
* Second number (let's call it *d*) is the number of dimensions
in the object array. Typically it's 2 (so, we usually deal with
2-dimensional object arrays).
* Next *d* numbers contain the size of each of all the dimensions.
So, for a single object we have 2 numbers: 1 and 1.
* Then, for each of the object in the object array, comes
**objectId**. Naturally, the number of the objects is just a
product of all the dimension sizes.
* Finally, the last number is **classId**.
So, the main information you can learn from this element is two numbers:
*objectId* is the index of object in the list of all the objects
stored in the file (numbering starts with 1), while *classId*
is the index of our object's class in the list of all the classes
that are present in the file (again, numbering starts with 1).
For example, if we only store one object in out file,
both of these numbers will be equal to 1.
These two numbers is all you need to find your object's data
in the subsystem data, so let's proceed to that.
## Subsystem data format
### General structure
First, comes a small header.
* `0x00`, `0x01` (2 bytes)
* `0x4d49` (`MI`, 16-bit integer): endian indicator (see page 1-7).
After that come variables that are stored in the same way as in the
"normal" portion of the file.
I think that there should be only two variables, and the second one does
not contain any useful information.
So we'll focus on the first variables, which should be a structure array
with a single field `MCOS` (MATLAB Class Object System, again).
The value of this field is an array of class `mxOPAQUE_CLASS` again,
so it has the structure described above. It is different, though:
its class name is now `FileWrapper__`, and its data is not a matrix of class
`mxUINT32` with object number and class number, but a cell array.
This cell array is what we are interested in.
Its first element is a byte array (matrix of class `mxUINT8`)
that ties together all the references to object data, i.e.,
object numbers and class numbers. Let's call this byte array
**object metadata**.
The second element doesn't really contain anything, as far as I can say:
it is an empty cell array.
The following elements contain the fields of the objects that are stored
in the file; the object metadata tells you which fields belong to which
objects. We shall refer to these elements as **field contents**.
Finally, the last element is of unknown purpose: in all the examples
I've seen, it contains a column cell array with several empty
structure arrays.
### Object metadata
Object metadata is a single byte array with several regions inside:
* **Table of contents**: a list of 16-bit integers:
* first is of unknown purpose;
* second is **number of field/class names** (see below);
* the rest are treated as offsets (in bytes) to the regions inside
object metdata. The offsets are listed in ascending order, and the list
is 0-terminated. Note that every region, including table of contents,
is aligned on 8-byte boundaries, so there might be extra 4 bytes of padding
after the terminating 0.
In all the examples I've seen, this list contains 6 offsets, and the last
one actually points to the end of object metadata (its value is the same
as the metadata length). Thus we shall assume that the object metadata has
6 regions: the last one is probably empty.
* Then come **field/class names**: a list of zero-terminated ASCII
strings. The number of the strings was mentioned above; again, the data is
aligned on 8-byte boundaries.
This region gives you names for all the classes that are stored in the file,
and all their fields. The order is arbitrary, and the information that
connects the field to classes (and tells you which of the names are
actually names of classes) comes later.
* Region 1 contains several blocks, four 32-bit integers in each block.
First block contains integers (0, 0, 0, 0), and the rest of the blocks
contain (0, *n*, 0, 0), where *n* is an index in the **field/class names**
list that points to a class name. Thus, the number of this 128-byte blocks
is the number of different classes stored in the file, plus one zero-filled
block in the beginning.
This allows you to get class names from region 1; the rest are field names.
Moreover, the order of the class names in this region corresponds to
the "classId" numbers, which allows you to connect class names to
classIds.
* Region 2 seems to be empty.
* Region 3 connects objects to classes. It contains a sequence of blocks,
six 32-bit integers in each block. First block is filled with zeros,
and the rest contain (*classId*, 0, 0, 0, *objectId*, *objectDependencyId*).
Each block describes one object, and tells you which class this object
has. The meaning of *objectDependencyId* is not exactly clear
(but see below for some hints). Note that *classId* and *objectId* here
are the same that you may find in the references stored in
"opaque" objects.
* Region 4 connects objects to their fields. This is a list
of 32-bit integers. The first two integers (i.e., first 8 bytes) are zero.
Then comes a sequence of blocks, one for each object, in the ascending order
of objectIds. Each block starts with one 32-bit integer containing the number
of sub-blocks to follow; a sub-block consists of three 32-bit integers.
Note that each block is aligned on 64-bit boundaries, so there might
be padding involved.
As we said, a block corresponds to an object. Its sub-blocks
correspond to the fields in this object, and contain the following three
numbers: (*fieldId*, 1, *fieldContentsId*).
Here *fieldId* allows you to get field name from the *field/class names*
list of Region 1, and *fieldContentsId* points to the location of the value
of this field in the current object. As we said earlier, the field contents
are stored in the same cell array where object metadata is the first element.
The fieldContentsId numbering, again, starts with 1, and does not include
the first two cells (the object metadata cell and the unknown stuff cell).
This all might seem a little complicated, but actually is rather simple.
We have four lists: classes, objects, fields, and field contents.
Region 1 connects classes to their names (stored in **field/class names**
list, which allows you to get names for the field, as well).
Region 3 connects objects to classes.
Region 4 connects objects to fields, and fields to field contents.
The meaning of *objectDependencyId* values in Region 3 is not totally clear,
but it seems to suggest an alternative numbering of objects, which takes
into account their dependencies: if one object contains another one as a
field, then the second one should be constructed before the first.
Thus, the *objectDependencyId* of the second object should be lower
that that of the first object.
Actually, this situation (objects as fields of other objects) contains
a caveat: if the field that is stored in the *field contents* cells is
itself an object, it is not wrapped in an "opaque object" class data type,
in contrast to the objects stored in the "normal" section of a .mat file.
Instead, a *field contents* cell just contains the `mxUINT32` matrix
that would be the "data reference" part of an "opaque object".
This can lead to hilarious results: if you have an object that has
a field containing a column matrix of type `int32` whose first element
is the magic number 0xdd000000, this object can be saved into a .mat file
using MATLAB, but cannot be read back, since MATLAB treats it as an object
reference and tries to extract its value based on the other values of the
matrix, which can easily lead to crashes or even infinite loops.

63
MatFileHandler/Opaque.cs Normal file
View File

@ -0,0 +1,63 @@
using System;
using System.Numerics;
namespace MatFileHandler
{
/// <summary>
/// Matlab "opaque object" structure.
/// If this object appears in the "main" section of the .mat file,
/// it just contains a small data structure pointing to the object's
/// storage in the "subsystem data" portion of the file.
/// In this case, an instance of <see cref="OpaqueLink"/> class
/// will be created.
/// If this object appears in the "subsystem data" part, it contains
/// the data of all opaque objects in the file, and that is what we
/// put into <see cref="RawData"/> property.
/// </summary>
internal class Opaque : MatArray, IArray
{
/// <summary>
/// Initializes a new instance of the <see cref="Opaque"/> class.
/// </summary>
/// <param name="name">Name of the object.</param>
/// <param name="typeDescription">Type description.</param>
/// <param name="className">Class name.</param>
/// <param name="dimensions">Dimensions of the object.</param>
/// <param name="rawData">Raw object's data.</param>
/// <param name="subsystemData">Subsystem data.</param>
public Opaque(string name, string typeDescription, string className, int[] dimensions, DataElement rawData, SubsystemData subsystemData)
: base(new ArrayFlags(ArrayType.MxOpaque, 0), dimensions, name)
{
TypeDescription = typeDescription ?? throw new ArgumentNullException(nameof(typeDescription));
ClassName = className ?? throw new ArgumentNullException(nameof(className));
RawData = rawData ?? throw new ArgumentNullException(nameof(rawData));
SubsystemData = subsystemData ?? throw new ArgumentNullException(nameof(subsystemData));
}
/// <summary>
/// Gets class name of the opaque object.
/// </summary>
public virtual string ClassName { get; }
/// <summary>
/// Gets raw object's data: either links to subsystem data, or actual data.
/// </summary>
public DataElement RawData { get; }
/// <summary>
/// Gets "type description" of the opaque object.
/// </summary>
public string TypeDescription { get; }
/// <summary>
/// Gets subsystem data.
/// </summary>
public SubsystemData SubsystemData { get; }
/// <inheritdoc />
public override Complex[]? ConvertToComplexArray() => null;
/// <inheritdoc />
public override double[]? ConvertToDoubleArray() => null;
}
}

View File

@ -0,0 +1,179 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace MatFileHandler
{
/// <summary>
/// Implementation of Matlab's "opaque objects" via links to subsystem data.
/// </summary>
internal class OpaqueLink : Opaque, IMatObject
{
/// <summary>
/// Initializes a new instance of the <see cref="OpaqueLink"/> class.
/// </summary>
/// <param name="name">Name of the object.</param>
/// <param name="typeDescription">Description of object's class.</param>
/// <param name="className">Name of the object's class.</param>
/// <param name="dimensions">Dimensions of the object.</param>
/// <param name="data">Raw data containing links to object's storage.</param>
/// <param name="indexToObjectId">Links to object's storage.</param>
/// <param name="classIndex">Index of object's class.</param>
/// <param name="subsystemData">Reference to global subsystem data.</param>
public OpaqueLink(
string name,
string typeDescription,
string className,
int[] dimensions,
DataElement data,
int[] indexToObjectId,
int classIndex,
SubsystemData subsystemData)
: base(name, typeDescription, className, dimensions, data, subsystemData)
{
IndexToObjectId = indexToObjectId ?? throw new ArgumentNullException(nameof(indexToObjectId));
ClassIndex = classIndex;
}
/// <summary>
/// Gets index of this object's class in subsystem data class list.
/// </summary>
public int ClassIndex { get; }
/// <inheritdoc />
public IReadOnlyDictionary<string, IArray>[] Data
{
get
{
var result = new IReadOnlyDictionary<string, IArray>[Count];
for (var i = 0; i < Count; i++)
{
result[i] = FieldNamesArray.ToDictionary(
name => name,
name => this[name, i]);
}
return result;
}
}
/// <inheritdoc />
public IEnumerable<string> FieldNames => FieldNamesArray;
/// <summary>
/// Gets links to the fields stored in subsystem data.
/// </summary>
public int[] IndexToObjectId { get; }
/// <summary>
/// Gets name of this object's class.
/// </summary>
public override string ClassName => SubsystemData.ClassInformation[ClassIndex].Name;
private string[] FieldNamesArray => SubsystemData.ClassInformation[ClassIndex].FieldNames.ToArray();
/// <inheritdoc />
public IArray this[string field, params int[] list]
{
get => TryGetValue(field, out var result, list) ? result! : throw new ArgumentOutOfRangeException(nameof(list));
set => throw new NotImplementedException();
}
/// <inheritdoc />
public IReadOnlyDictionary<string, IArray> this[params int[] list]
{
get => ExtractObject(Dimensions.DimFlatten(list));
set => throw new NotImplementedException();
}
private OpaqueObjectArrayElement ExtractObject(int i)
{
return new OpaqueObjectArrayElement(this, i);
}
private bool TryGetValue(string field, out IArray? output, params int[] list)
{
var index = Dimensions.DimFlatten(list);
var maybeFieldIndex = SubsystemData.ClassInformation[ClassIndex].FindField(field);
if (maybeFieldIndex is not int fieldIndex)
{
output = default;
return false;
}
if (index >= IndexToObjectId.Length)
{
output = default;
return false;
}
var objectId = IndexToObjectId[index];
var objectInfo = SubsystemData.ObjectInformation[objectId];
var fieldId = objectInfo.FieldLinks[fieldIndex];
output = SubsystemData.Data[fieldId];
return true;
}
/// <summary>
/// Provides access to a single object in object array.
/// </summary>
internal class OpaqueObjectArrayElement : IReadOnlyDictionary<string, IArray>
{
private readonly int index;
private readonly OpaqueLink parent;
/// <summary>
/// Initializes a new instance of the <see cref="OpaqueObjectArrayElement"/> class.
/// </summary>
/// <param name="parent">Parent object array.</param>
/// <param name="index">Index of the object in the array.</param>
public OpaqueObjectArrayElement(OpaqueLink parent, int index)
{
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
this.index = index;
}
/// <inheritdoc />
public int Count => parent.FieldNamesArray.Length;
/// <inheritdoc />
public IEnumerable<string> Keys => parent.FieldNames;
/// <inheritdoc />
public IEnumerable<IArray> Values => parent.FieldNames.Select(fieldName => parent[fieldName, index]);
/// <inheritdoc />
public IArray this[string key] => parent[key, index];
/// <inheritdoc />
public bool ContainsKey(string key)
{
return parent.FieldNames.Contains(key);
}
/// <inheritdoc />
public IEnumerator<KeyValuePair<string, IArray>> GetEnumerator()
{
foreach (var field in parent.FieldNamesArray)
{
yield return new KeyValuePair<string, IArray>(field, parent[field, index]);
}
}
#pragma warning disable CS8767
/// <inheritdoc />
public bool TryGetValue(string key, out IArray? value)
#pragma warning restore CS8767
{
return parent.TryGetValue(key, out value, index);
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.IO;
namespace MatFileHandler;
/// <summary>
/// A stream which wraps another stream and tracks the number of bytes read
/// for the purpose of adjusting for padding.
/// </summary>
internal sealed class PositionTrackingStream : Stream
{
private readonly Stream _baseStream;
private long _position;
/// <summary>
/// Initializes a new instance of the <see cref="PositionTrackingStream"/> class.
/// </summary>
/// <param name="baseStream">The stream to wrap.</param>
public PositionTrackingStream(Stream baseStream)
{
_baseStream = baseStream;
}
/// <inheritdoc/>
public override bool CanRead => _baseStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length => _baseStream.Length;
/// <inheritdoc/>
public override long Position
{
get => _position;
set => throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Flush() => _baseStream.Flush();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
var bytesRead = _baseStream.Read(buffer, offset, count);
_position += bytesRead;
return bytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_baseStream.Dispose();
}
base.Dispose(disposing);
}
}

View File

@ -0,0 +1,34 @@
using System;
namespace MatFileHandler
{
/// <summary>
/// Raw variable read from the file.
/// This gives a way to deal with "subsystem data" which looks like
/// a variable and can only be detected by comparing its offset with
/// the value stored in the file's header.
/// </summary>
internal class RawVariable
{
/// <summary>
/// Initializes a new instance of the <see cref="RawVariable"/> class.
/// </summary>
/// <param name="offset">Offset of the variable in the source file.</param>
/// <param name="dataElement">Data element parsed from the file.</param>
internal RawVariable(long offset, DataElement dataElement)
{
Offset = offset;
DataElement = dataElement ?? throw new ArgumentNullException(nameof(dataElement));
}
/// <summary>
/// Gets data element with the variable's contents.
/// </summary>
public DataElement DataElement { get; }
/// <summary>
/// Gets offset of the variable in the .mat file.
/// </summary>
public long Offset { get; }
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Text;
namespace MatFileHandler
{
/// <summary>
/// A better interface for using string objects.
/// </summary>
public class StringAdapter
{
private readonly string[] strings;
/// <summary>
/// Initializes a new instance of the <see cref="StringAdapter"/> class.
/// </summary>
/// <param name="array">Source string object.</param>
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<ulong>
?? throw new HandlerException("Cannot extract string data.");
(Dimensions, strings) = ParseBinaryData(binaryData.Data);
}
/// <summary>
/// Gets string array dimensions.
/// </summary>
public int[] Dimensions { get; }
/// <summary>
/// Gets string object at given position.
/// </summary>
/// <param name="list">Indices.</param>
/// <returns>Value.</returns>
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);
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.IO;
namespace MatFileHandler
{
/// <summary>
/// A stream which reads a finite section of another stream.
/// </summary>
internal sealed class Substream : Stream
{
private readonly Stream _baseStream;
private long _bytesRead;
/// <summary>
/// Initializes a new instance of the <see cref="Substream"/> class.
/// </summary>
/// <param name="baseStream">The <see cref="Stream"/> to wrap.</param>
/// <param name="length">The number of bytes readable from this <see cref="Substream"/>.</param>
public Substream(Stream baseStream, long length)
{
_baseStream = baseStream;
Length = length;
}
/// <inheritdoc/>
public override bool CanRead => _baseStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length { get; }
/// <inheritdoc/>
public override long Position
{
get => _bytesRead;
set => throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Flush() => _baseStream.Flush();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
var bytesRead = _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - _bytesRead));
_bytesRead += bytesRead;
return bytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
}
}

View File

@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
namespace MatFileHandler
{
/// <summary>
/// "Subsystem data" of the .mat file.
/// Subsystem data stores the actual contents
/// of all the "opaque objects" in the file.
/// </summary>
internal class SubsystemData
{
private RealSubsystemData? _realData;
/// <summary>
/// Initializes a new instance of the <see cref="SubsystemData"/> class.
/// Default constructor: initializes everything to null.
/// </summary>
public SubsystemData()
{
_realData = null;
}
/// <summary>
/// Initializes a new instance of the <see cref="SubsystemData"/> class.
/// The actual constructor.
/// </summary>
/// <param name="classInformation">Information about the classes.</param>
/// <param name="objectInformation">Information about the objects.</param>
/// <param name="fieldNames">Field names.</param>
/// <param name="data">Field values.</param>
public SubsystemData(
Dictionary<int, ClassInfo> classInformation,
Dictionary<int, ObjectInfo> objectInformation,
string[] fieldNames,
Dictionary<int, IArray> data)
{
_realData = new RealSubsystemData(
classInformation,
objectInformation,
fieldNames,
data);
}
/// <summary>
/// Gets information about all the classes occurring in the file.
/// </summary>
public Dictionary<int, ClassInfo> ClassInformation =>
_realData?.ClassInformation ?? throw new HandlerException("Subsystem data missing.");
/// <summary>
/// Gets the actual data: mapping of "object field" indices to their values.
/// </summary>
public IReadOnlyDictionary<int, IArray> Data =>
_realData?.Data ?? throw new HandlerException("Subsystem data missing.");
/// <summary>
/// Gets information about all the objects occurring in the file.
/// </summary>
public Dictionary<int, ObjectInfo> ObjectInformation =>
_realData?.ObjectInformation ?? throw new HandlerException("Subsystem data missing.");
/// <summary>
/// Gets field names.
/// </summary>
public string[] FieldNames =>
_realData?.FieldNames ?? throw new HandlerException("Subsystem data missing.");
/// <summary>
/// Initialize this object from another object.
/// This ugly hack allows us to read the opaque objects and store references to
/// the subsystem data in them before parsing the actual subsystem data (which
/// comes later in the file).
/// </summary>
/// <param name="data">Another subsystem data.</param>
public void Set(SubsystemData data)
{
_realData = new RealSubsystemData(
data.ClassInformation,
data.ObjectInformation,
data.FieldNames,
data.Data);
}
/// <summary>
/// Stores information about a class.
/// </summary>
internal class ClassInfo
{
private readonly Dictionary<string, int> fieldToIndex;
/// <summary>
/// Initializes a new instance of the <see cref="ClassInfo"/> class.
/// </summary>
/// <param name="name">Class name.</param>
/// <param name="fieldToIndex">A dictionary mapping field names to field ids.</param>
public ClassInfo(string name, Dictionary<string, int> fieldToIndex)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
this.fieldToIndex = fieldToIndex ?? throw new ArgumentNullException(nameof(fieldToIndex));
}
/// <summary>
/// Gets names of the fields in the class.
/// </summary>
public IReadOnlyCollection<string> FieldNames => fieldToIndex.Keys;
/// <summary>
/// Gets name of the class.
/// </summary>
public string Name { get; }
/// <summary>
/// Find a field index given its name.
/// </summary>
/// <param name="fieldName">Field name.</param>
/// <returns>Field index.</returns>
public int? FindField(string fieldName)
{
return fieldToIndex.TryGetValue(fieldName, out var index) ? index : null;
}
}
/// <summary>
/// Stores information about an object.
/// </summary>
internal class ObjectInfo
{
private readonly Dictionary<int, int> fieldLinks;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectInfo"/> class.
/// </summary>
/// <param name="fieldLinks">A dictionary mapping the field indices to "field values" indices.</param>
public ObjectInfo(Dictionary<int, int> fieldLinks)
{
this.fieldLinks = fieldLinks ?? throw new ArgumentNullException(nameof(fieldLinks));
}
/// <summary>
/// Gets mapping between the field indices and "field values" indices.
/// </summary>
public IReadOnlyDictionary<int, int> FieldLinks => fieldLinks;
}
private class RealSubsystemData
{
/// <summary>
/// Initializes a new instance of the <see cref="RealSubsystemData"/> class.
/// </summary>
/// <param name="classInformation">Class information.</param>
/// <param name="objectInformation">Object information.</param>
/// <param name="fieldNames">Field names.</param>
/// <param name="data">Data.</param>
public RealSubsystemData(
Dictionary<int, ClassInfo> classInformation,
Dictionary<int, ObjectInfo> objectInformation,
string[] fieldNames,
IReadOnlyDictionary<int, IArray> data)
{
ClassInformation = classInformation ?? throw new ArgumentNullException(nameof(classInformation));
ObjectInformation = objectInformation ?? throw new ArgumentNullException(nameof(objectInformation));
FieldNames = fieldNames;
Data = data ?? throw new ArgumentNullException(nameof(data));
}
/// <summary>
/// Gets information about all the classes occurring in the file.
/// </summary>
public Dictionary<int, ClassInfo> ClassInformation { get; }
/// <summary>
/// Gets the actual data: mapping of "object field" indices to their values.
/// </summary>
public IReadOnlyDictionary<int, IArray> Data { get; }
/// <summary>
/// Gets information about all the objects occurring in the file.
/// </summary>
public Dictionary<int, ObjectInfo> ObjectInformation { get; }
/// <summary>
/// Gets field names.
/// </summary>
public string[] FieldNames { get; }
}
}
}

View File

@ -0,0 +1,412 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace MatFileHandler
{
/// <summary>
/// Reader for "subsystem data" in .mat files.
/// </summary>
internal class SubsystemDataReader
{
/// <summary>
/// Read subsystem data from a given byte array.
/// </summary>
/// <param name="bytes">Byte array with the data.</param>
/// <param name="subsystemData">
/// Link to the existing subsystem data; this will be put in nested OpaqueLink objects
/// and later replaced with the subsystem data that we are currently reading.</param>
/// <returns>Subsystem data read.</returns>
public static SubsystemData Read(byte[] bytes, SubsystemData subsystemData)
{
var rawVariables = ReadRawVariables(bytes, subsystemData);
// Parse subsystem data.
var mainVariable = rawVariables[0].DataElement as IStructureArray
?? throw new HandlerException("Subsystem data must be a structure array.");
var mcosData = mainVariable["MCOS", 0] as Opaque
?? throw new HandlerException("MCOS data must be an opaque object.");
var opaqueData = mcosData.RawData as ICellArray
?? throw new HandlerException("Opaque data must be a cell array.");
var info = (opaqueData[0] as IArrayOf<byte>)?.Data
?? throw new HandlerException("Opaque data info must be a byte array.");
var (offsets, position) = ReadOffsets(info, 0);
var fieldNames = ReadFieldNames(info, position, offsets[1]);
var numberOfClasses = ((offsets[3] - offsets[2]) / 16) - 1;
var classIdToName = ReadClassIdToName(info, offsets, fieldNames, numberOfClasses);
var numberOfEmbeddedObjects = (offsets[4] - offsets[3] - 8) / 16;
var embeddedObjectPositionsToValues = ReadEmbeddedObjectPositionsToValues(info, offsets, numberOfEmbeddedObjects);
var numberOfObjects = ((offsets[5] - offsets[4]) / 24) - 1;
var objectClasses = ReadObjectClassInformations(info, offsets, numberOfObjects);
var numberOfObjectPositions = objectClasses.NumberOfObjectPositions;
var objectPositionsToValues = ReadObjectPositionsToValues(info, offsets, numberOfObjectPositions);
var (classInformation, objectInformation) =
GatherClassAndObjectInformation(
classIdToName,
fieldNames,
objectClasses,
objectPositionsToValues,
embeddedObjectPositionsToValues);
var allFields = objectInformation.Values.SelectMany(obj => obj.FieldLinks.Values);
var data = new Dictionary<int, IArray>();
foreach (var i in allFields)
{
data[i] = TransformOpaqueData(opaqueData[i + 2], subsystemData);
}
return new SubsystemData(classInformation, objectInformation, fieldNames, data);
}
private static Dictionary<int, Dictionary<int, int>> ReadObjectPositionsToValues(byte[] info, int[] offsets, int numberOfObjectPositions)
{
using var stream = new MemoryStream(info, offsets[5], offsets[6] - offsets[5]);
using var reader = new BinaryReader(stream);
return ReadObjectPositionsToValuesMapping(reader, numberOfObjectPositions);
}
private static ObjectClasses ReadObjectClassInformations(byte[] info, int[] offsets, int numberOfObjects)
{
using var stream = new MemoryStream(info, offsets[4], offsets[5] - offsets[4]);
using var reader = new BinaryReader(stream);
return ReadObjectClasses(reader, numberOfObjects);
}
private static Dictionary<int, Dictionary<int, int>> ReadEmbeddedObjectPositionsToValues(byte[] info, int[] offsets, int numberOfEmbeddedObjects)
{
using var stream = new MemoryStream(info, offsets[3], offsets[4] - offsets[3]);
using var reader = new BinaryReader(stream);
return ReadEmbeddedObjectPositionsToValuesMapping(reader, numberOfEmbeddedObjects);
}
private static Dictionary<int, string> ReadClassIdToName(byte[] info, int[] offsets, string[] fieldNames, int numberOfClasses)
{
using var stream = new MemoryStream(info, offsets[2], offsets[3] - offsets[2]);
using var reader = new BinaryReader(stream);
return ReadClassNames(reader, fieldNames, numberOfClasses);
}
private static List<RawVariable> ReadRawVariables(byte[] bytes, SubsystemData subsystemData)
{
using var stream = new MemoryStream(bytes);
using var reader = new BinaryReader(stream);
reader.ReadBytes(8);
return MatFileReader.ReadRawVariables(reader, -1, subsystemData);
}
private static (Dictionary<int, SubsystemData.ClassInfo> classInfos,
Dictionary<int, SubsystemData.ObjectInfo> objectInfos)
GatherClassAndObjectInformation(
Dictionary<int, string> classIdToName,
string[] fieldNames,
ObjectClasses objectClasses,
Dictionary<int, Dictionary<int, int>> objectPositionsToValues,
Dictionary<int, Dictionary<int, int>> embeddedObjectPositionsToValues)
{
var classInfos = new Dictionary<int, SubsystemData.ClassInfo>();
foreach (var classId in classIdToName.Keys)
{
var className = classIdToName[classId];
var fieldIds = new SortedSet<int>();
foreach (var objectPosition in objectPositionsToValues.Keys)
{
var foundClassId = objectClasses.GetClassIdByObjectPosition(objectPosition);
if (foundClassId != classId)
{
continue;
}
foreach (var fieldId in objectPositionsToValues[objectPosition].Keys)
{
fieldIds.Add(fieldId);
}
}
foreach (var objectPosition in embeddedObjectPositionsToValues.Keys)
{
var foundClassId = objectClasses.GetClassIdByEmbeddedObjectPosition(objectPosition);
if (foundClassId != classId)
{
continue;
}
foreach (var fieldId in embeddedObjectPositionsToValues[objectPosition].Keys)
{
fieldIds.Add(fieldId);
}
}
var fieldToIndex = new Dictionary<string, int>();
foreach (var fieldId in fieldIds)
{
fieldToIndex[fieldNames[fieldId - 1]] = fieldId;
}
classInfos[classId] = new SubsystemData.ClassInfo(className, fieldToIndex);
}
var objectInfos = new Dictionary<int, SubsystemData.ObjectInfo>();
foreach (var objectPosition in objectPositionsToValues.Keys)
{
var foundKey = objectClasses.GetKeyByObjectPosition(objectPosition);
objectInfos[foundKey] = new SubsystemData.ObjectInfo(objectPositionsToValues[objectPosition]);
}
foreach (var embeddedObjectPosition in embeddedObjectPositionsToValues.Keys)
{
var foundKey = objectClasses.GetKeyByEmbeddedObjectPosition(embeddedObjectPosition);
objectInfos[foundKey] = new SubsystemData.ObjectInfo(embeddedObjectPositionsToValues[embeddedObjectPosition]);
}
return (classInfos, objectInfos);
}
private static Dictionary<int, string> ReadClassNames(
BinaryReader reader,
string[] fieldNames,
int numberOfClasses)
{
var result = new Dictionary<int, string>();
var indices = new int[numberOfClasses + 1];
for (var i = 0; i <= numberOfClasses; i++)
{
reader.ReadInt32();
indices[i] = reader.ReadInt32();
reader.ReadInt32();
reader.ReadInt32();
}
for (var i = 0; i < numberOfClasses; i++)
{
result[i + 1] = fieldNames[indices[i + 1] - 1];
}
return result;
}
private static Dictionary<int, Dictionary<int, int>> ReadEmbeddedObjectPositionsToValuesMapping(
BinaryReader reader, int numberOfObjects)
{
var result = new Dictionary<int, Dictionary<int, int>>();
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<int, int> { [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<byte>();
while (bytes[position] != 0)
{
list.Add(bytes[position]);
position++;
}
result[i] = Encoding.ASCII.GetString(list.ToArray());
position++;
}
return result;
}
private static Dictionary<int, int> ReadFieldToFieldDataMapping(BinaryReader reader)
{
var length = reader.ReadInt32();
var result = new Dictionary<int, int>();
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 ObjectClasses ReadObjectClasses(
BinaryReader reader,
int numberOfObjects)
{
var result = new Dictionary<int, ObjectClassInformation>();
var classIdFromObjectPosition = new Dictionary<int, int>();
var classIdFromEmbeddedObjectPosition = new Dictionary<int, int>();
var keyFromObjectPosition = new Dictionary<int, int>();
var keyFromEmbeddedObjectPosition = new Dictionary<int, int>();
reader.ReadBytes(24);
var numberOfObjectPositions = 0;
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(); // Not used.
classIdFromObjectPosition[objectPosition] = classId;
classIdFromEmbeddedObjectPosition[embeddedObjectPosition] = classId;
keyFromObjectPosition[objectPosition] = i + 1;
keyFromEmbeddedObjectPosition[embeddedObjectPosition] = i + 1;
if (objectPosition != 0)
{
numberOfObjectPositions++;
}
}
return new ObjectClasses(
classIdFromObjectPosition,
classIdFromEmbeddedObjectPosition,
keyFromObjectPosition,
keyFromEmbeddedObjectPosition,
numberOfObjectPositions);
}
private static Dictionary<int, Dictionary<int, int>> ReadObjectPositionsToValuesMapping(
BinaryReader reader,
int numberOfObjects)
{
var result = new Dictionary<int, Dictionary<int, int>>();
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;
var offsets = new List<int>();
while (true)
{
var next = BitConverter.ToInt32(bytes, position);
position += 4;
if (next == bytes.Length)
{
break;
}
offsets.Add(next);
}
return (offsets.ToArray(), 40);
}
private static IArray TransformOpaqueData(IArray array, SubsystemData subsystemData)
{
if (array is MatNumericalArrayOf<uint> uintArray &&
uintArray.Data.Length == 6 &&
uintArray.Data[0] == 3707764736u)
{
return ObjectParser.ParseObject(uintArray, subsystemData);
}
if (array is MatCellArray cellArray)
{
for (var i = 0; i < cellArray.Data.Length; i++)
{
var cell = cellArray.Data[i];
var transformedCell = TransformOpaqueData(cell, subsystemData);
cellArray.Data[i] = transformedCell;
}
}
if (array is MatStructureArray structureArray)
{
var newFields = new Dictionary<string, List<IArray>>();
foreach (var pair in structureArray.Fields)
{
var values = pair.Value;
var transformedValues = new List<IArray>(values.Count);
foreach (var value in values)
{
var transformedValue = TransformOpaqueData(value, subsystemData);
transformedValues.Add(transformedValue);
}
newFields[pair.Key] = transformedValues;
}
foreach (var pair in newFields)
{
structureArray.Fields[pair.Key] = pair.Value;
}
}
return array;
}
private readonly 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; }
}
private class ObjectClasses
{
private readonly Dictionary<int, int> _classIdFromObjectPosition;
private readonly Dictionary<int, int> _classIdFromEmbeddedObjectPosition;
private readonly Dictionary<int, int> _keyFromObjectPosition;
private readonly Dictionary<int, int> _keyFromEmbeddedObjectPosition;
public ObjectClasses(
Dictionary<int, int> classIdFromObjectPosition,
Dictionary<int, int> classIdFromEmbeddedObjectPosition,
Dictionary<int, int> keyFromObjectPosition,
Dictionary<int, int> keyFromEmbeddedObjectPosition,
int numberOfObjectPositions)
{
_classIdFromObjectPosition = classIdFromObjectPosition;
_classIdFromEmbeddedObjectPosition = classIdFromEmbeddedObjectPosition;
_keyFromObjectPosition = keyFromObjectPosition;
_keyFromEmbeddedObjectPosition = keyFromEmbeddedObjectPosition;
NumberOfObjectPositions = numberOfObjectPositions;
}
public int NumberOfObjectPositions { get; }
public int GetClassIdByObjectPosition(int objectPosition)
=> _classIdFromObjectPosition[objectPosition];
public int GetClassIdByEmbeddedObjectPosition(int embeddedObjectPosition)
=> _classIdFromEmbeddedObjectPosition[embeddedObjectPosition];
public int GetKeyByObjectPosition(int objectPosition)
=> _keyFromObjectPosition[objectPosition];
public int GetKeyByEmbeddedObjectPosition(int embeddedObjectPosition)
=> _keyFromEmbeddedObjectPosition[embeddedObjectPosition];
}
}
}

View File

@ -0,0 +1,99 @@
using System;
using System.Linq;
namespace MatFileHandler
{
/// <summary>
/// A better interface for using table objects.
/// </summary>
public class TableAdapter
{
private readonly IMatObject matObject;
/// <summary>
/// Initializes a new instance of the <see cref="TableAdapter"/> class.
/// </summary>
/// <param name="array">Source table object.</param>
public TableAdapter(IArray array)
{
matObject = array as IMatObject
?? throw new HandlerException("Table adapter must be initialized with a MATLAB object.");
if (matObject.ClassName != "table")
{
throw new ArgumentException("The object provided is not a table.");
}
var cellArray = matObject["varnames"] as ICellArray
?? throw new HandlerException("Table variable names must be in a cell array.");
VariableNames = Enumerable
.Range(0, cellArray.Count)
.Select(i =>
(cellArray[i] as ICharArray ??
throw new HandlerException("Variable name must be a char array.")).String)
.ToArray();
NumberOfVariables = VariableNames.Length;
var props = matObject["props"] as IStructureArray
?? throw new HandlerException("Table properties must be a structure array.");
Description = (props["Description"] as ICharArray
?? throw new HandlerException("Table description must be a char array.")).String;
NumberOfRows = (int)(matObject["nrows"].ConvertToDoubleArray()
?? throw new HandlerException("Cannot find number of rows in a table."))[0];
var rowNamesArrays = matObject["rownames"] as ICellArray
?? throw new HandlerException("Table row names must be a cell array.");
RowNames = Enumerable
.Range(0, rowNamesArrays.Count)
.Select(i => (cellArray[i] as ICharArray
?? throw new HandlerException("Each table row name must be a char array.")).String)
.ToArray();
}
/// <summary>
/// Gets table description.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the number of rows in the table.
/// </summary>
public int NumberOfRows { get; }
/// <summary>
/// Gets the number of variables in the table.
/// </summary>
public int NumberOfVariables { get; }
/// <summary>
/// Gets row names.
/// </summary>
public string[] RowNames { get; }
/// <summary>
/// Gets variable names.
/// </summary>
public string[] VariableNames { get; }
/// <summary>
/// Gets all the data for a given variable.
/// </summary>
/// <param name="variableName">Variable name.</param>
/// <returns>All data associated with the variable.</returns>
public IArray this[string variableName]
{
get
{
var maybeIndex = Enumerable.Range(0, VariableNames.Length)
.Where(i => VariableNames[i] == variableName)
.Select(i => (int?)i)
.FirstOrDefault();
if (maybeIndex is not int index)
{
throw new ArgumentOutOfRangeException(nameof(variableName), $"Variable '{variableName}' not found.");
}
var data = matObject["data"] as ICellArray
?? throw new HandlerException("Table data must be stored in a cell array.");
return data[index];
}
}
}
}

View File

@ -1,5 +1,3 @@
// Copyright 2017 Alexander Luzgarev
namespace MatFileHandler
{
/// <summary>
@ -114,4 +112,4 @@ namespace MatFileHandler
/// </summary>
public int ElementSize => Type.Size();
}
}
}

207
README.md
View File

@ -1,7 +1,32 @@
# MatFileHandler
This document briefly describes how to perform simple operations with .mat files using MatFileHandler.
If you have questions and/or ideas, you can [file a new issue](https://github.com/mahalex/MatFileHandler/issues/new) or contact me directly at <mahalex@gmail.com>.
[![NuGet Version](https://img.shields.io/nuget/vpre/MatFileHandler?color=green)](https://www.nuget.org/packages/MatFileHandler/absoluteLatest)
MatFileHandler is a .NET library for reading and writing MATLAB .mat files
(of so-called "Level 5"). MatFileHandler supports numerical arrays,
logical arrays, sparse arrays, char arrays, cell arrays and structure arrays.
Moreover, MatFileHandler is able to read the contents of MATLAB objects,
and is currently probably the only third-party library that can do that.
Since the format for storing MATLAB objects seems to be obscure and
undocumented, support for them is preliminary and probably contains bugs.
You can find (partial) technical description of MATLAB object data format
[here](MatFileHandler/Objects.md).
This document briefly describes how to perform simple operations with .mat files
using MatFileHandler.
If you have questions and/or ideas, you can [file a new issue](https://git.mahalex.net/mahalex/MatFileHandler/issues/new)
or contact me directly at <mahalex@gmail.com>.
## Changelog
* Version `1.3.0` adds (read-only) support for Matlab objects, as well as an
interface to read tables.
* Version `1.2.0` makes data compression when writing files optional.
* Version `1.1.0` adds multi-targeting: the project now targets .NET Framework
4.6.1 as well as .NET Standard 2.0.
## Reading a .mat file
@ -24,9 +49,11 @@ foreach (IVariable variable in matFile.Variables) {
// Do stuff
}
```
(all of the interfaces and classes described in this text are in the `MatFileHandler` namespace).
(all of the interfaces and classes described in this text are in the
`MatFileHandler` namespace).
Each `IVariable` has a name, a value, and a flag indicating if it's a “global” variable:
Each `IVariable` has a name, a value, and a flag indicating if it's a “global”
variable:
```csharp
public interface IVariable
{
@ -36,8 +63,12 @@ public interface IVariable
}
```
The interesting part here is the `IArray` interface. This is a base interface, which is extended by other interfaces that provide access to more specific MATLAB arrays (numerical, cell, structure, char, etc.).
We can't do much with `IArray` itself: check for emptiness, get its dimensions and total number of elements in it, or try to convert it to an array of double (or complex) numbers:
The interesting part here is the `IArray` interface. This is a base interface,
which is extended by other interfaces that provide access to more specific
MATLAB arrays (numerical, cell, structure, char, etc.).
We can't do much with `IArray` itself: check for emptiness, get its dimensions
and total number of elements in it, or try to convert it to an array of double
(or complex) numbers:
```csharp
public interface IArray
{
@ -49,11 +80,22 @@ public interface IArray
}
```
Note that `Dimensions` is a list, since all arrays in MATLAB are (at least potentially) multi-dimensional. However, `ConvertToDoubleArray()` and `ConvertToComplexArray()` return flat arrays, arranging all multi-dimensional data in columns (MATLAB-style). This functions return `null` if conversion failed (for example, if you tried to apply it to a structure array, or cell array).
Note that `Dimensions` is a list, since all arrays in MATLAB are (at least
potentially) multi-dimensional. However, `ConvertToDoubleArray()` and
`ConvertToComplexArray()` return flat arrays, arranging all multi-dimensional
data in columns (MATLAB-style). This functions return `null` if conversion
failed (for example, if you tried to apply it to a structure array, or cell
array).
## Numerical and logical arrays
### Numerical and logical arrays
The simplest type of array is a numerical array, which implements the `IArrayOf<T>` interface, where `T` is a numerical type, i. e., one of `Int8`, `UInt8`, `Int16`, `UInt16`, `Int32`, `UInt32`, `Int64`, `UInt64`, `Single`, `Double`. Arrays can contain complex values, which are just pairs of ordinary numbers. These pairs of `Double`s are represented by `System.Numerics.Complex`, and pairs of other numerical types are represented by a simple `ComplexOf<T>` struct, which has two properties:
The simplest type of array is a numerical array, which implements the
`IArrayOf<T>` interface, where `T` is a numerical type, i. e., one of `Int8`,
`UInt8`, `Int16`, `UInt16`, `Int32`, `UInt32`, `Int64`, `UInt64`, `Single`,
`Double`. Arrays can contain complex values, which are just pairs of
ordinary numbers. These pairs of `Double`s are represented by
`System.Numerics.Complex`, and pairs of other numerical types are
represented by a simple `ComplexOf<T>` struct, which has two properties:
```csharp
public struct ComplexOf<T> : IEquatable<ComplexOf<T>>
where T : struct
@ -64,9 +106,18 @@ public struct ComplexOf<T> : IEquatable<ComplexOf<T>>
}
```
All of this means that you can also have an `IArrayOf<T>` for `T` being `ComplexOf<Int8>`, `ComplexOf<UInt8>`, `ComplexOf<Int16>`, `ComplexOf<UInt16>`, `ComplexOf<Int32>`, `ComplexOf<UInt32>`, `ComplexOf<Int64>`, `ComplexOf<UInt64>`, `ComplexOf<Single>`, and, of course, `Complex` (note that we don't use `ComplexOf<Double>`). Finally, you can access a logical array as `IArrayOf<Boolean>`.
All of this means that you can also have an `IArrayOf<T>` for `T` being
`ComplexOf<Int8>`, `ComplexOf<UInt8>`, `ComplexOf<Int16>`, `ComplexOf<UInt16>`,
`ComplexOf<Int32>`, `ComplexOf<UInt32>`, `ComplexOf<Int64>`,
`ComplexOf<UInt64>`, `ComplexOf<Single>`, and, of course, `Complex` (note that
we don't use `ComplexOf<Double>`). Finally, you can access a logical array as
`IArrayOf<Boolean>`.
The `IArrayOf<T>` interface allows you to refer to a specific element by using a (multi-dimensional) indexer, or get all data at once as a flat array (multidimensional arrays get converted to flat using MATLAB conventions). Indexes start with 0 (note that in MATLAB they start with 1, so there is a shift in notation).
The `IArrayOf<T>` interface allows you to refer to a specific element by using a
(multi-dimensional) indexer, or get all data at once as a flat array
(multidimensional arrays get converted to flat using MATLAB conventions).
Indexes start with 0 (note that in MATLAB they start with 1, so there is a
shift in notation).
```csharp
public interface IArrayOf<T> : IArray
{
@ -74,26 +125,46 @@ public interface IArrayOf<T> : IArray
T this[params int[] list] { get; set; }
}
```
You can use a one-dimensional indexer or a multi-dimensional one, which is consistent with MATLAB notation. For example, a 2×3 array named `a` has elements `a[0, 0]`, `a[1, 0]` (first column), `a[0, 1]`, `a[1, 1]` (second column), `a[0, 2]`, `a[1, 2]` (third column), which can also be accessed as `a[0]`, `a[1]`, `a[2]`, `a[3]`, `a[4]`, and `a[5]`, respectively.
You can use a one-dimensional indexer or a multi-dimensional one, which is
consistent with MATLAB notation. For example, a 2×3 array named `a` has elements
`a[0, 0]`, `a[1, 0]` (first column), `a[0, 1]`, `a[1, 1]` (second column), `a[0,
2]`, `a[1, 2]` (third column), which can also be accessed as `a[0]`, `a[1]`, `a
[2]`, `a[3]`, `a[4]`, and `a[5]`, respectively.
## Cell arrays
### Cell arrays
Cell array is just an array of arrays, so `ICellArray` implements `IArrayOf<IArray>`, and adds nothing to it. This means that you can refer to specific cells in a cell array by using the indexer, or by inspecting the `Data` array described in the previous section.
Cell array is just an array of arrays, so `ICellArray` implements
`IArrayOf<IArray>`, and adds nothing to it. This means that you can refer to
specific cells in a cell array by using the indexer, or by inspecting the
`Data` array described in the previous section.
## Char arrays
### Char arrays
Char arrays implement `IArrayOf<char>`, so you can refer to individual chars in it via an indexer. Often a char array is used to carry a string, so there is a property for that:
Char arrays implement `IArrayOf<char>`, so you can refer to individual chars in
it via an indexer. Often a char array is used to carry a string, so there is
a property for that:
```csharp
public interface ICharArray : IArrayOf<char>
{
string String { get; }
}
```
This can be slightly weird for multi-dimensional arrays: the characters are stuffed into this string by columns (the same way the numerical array elements are flattened into a one-dimensional array). Moreover, each character array you read from a file actually implements either `IArrayOf<UInt8>`, or `IArrayOf<UInt16>`, depending on whether it was stored as a UTF-8 or UTF-16 encoded string. Characters arrays produced by MatFileHandler are always encoded as UTF-16.
This can be slightly weird for multi-dimensional arrays: the characters are
stuffed into this string by columns (the same way the numerical array elements
are flattened into a one-dimensional array). Moreover, each character array you
read from a file actually implements either `IArrayOf<UInt8>`, or
`IArrayOf<UInt16>`, depending on whether it was stored as a UTF-8 or UTF-16
encoded string. Characters arrays produced by MatFileHandler are always encoded
as UTF-16.
## Structure arrays
### Structure arrays
Structure arrays have elements that are indexed not only by their positions in the array, but also by structure fields. For example, a 1×2 structure array `s` with fields `x` and `y` has four elements: `s(1).x`, `s(1).y`, `s(2).x`, `s(2).y` (in MATLAB notation). This means that if you only specify the numerical indices, you get a dictionary that maps `string` to `IArray`; in order to reach a specific element, you need to provide both the indices and the field name:
Structure arrays have elements that are indexed not only by their positions in
the array, but also by structure fields. For example, a 1×2 structure array `s`
with fields `x` and `y` has four elements: `s(1).x`, `s(1).y`, `s(2).x`, `s
(2).y` (in MATLAB notation). This means that if you only specify the numerical
indices, you get a dictionary that maps `string` to `IArray`; in order to reach
a specific element, you need to provide both the indices and the field name:
```csharp
public interface IStructureArray : IArrayOf<IReadOnlyDictionary<string, IArray>>
{
@ -103,9 +174,10 @@ public interface IStructureArray : IArrayOf<IReadOnlyDictionary<string, IArray>>
```
Here `FieldNames` gives you a list of all fields in the structure.
## Sparse arrays
### Sparse arrays
Sparse array is like a numerical array, but not all of the values in it have to be specified; the rest are assumed to be 0.
Sparse array is like a numerical array, but not all of the values in it have to
be specified; the rest are assumed to be 0.
```csharp
public interface ISparseArrayOf<T> : IArrayOf<T>
where T : struct
@ -113,19 +185,99 @@ public interface ISparseArrayOf<T> : IArrayOf<T>
new IReadOnlyDictionary<(int, int), T> Data { get; }
}
```
Since `ISparseArrayOf<T>` implements `IArrayOf<T>`, you still can access all the elements in a sparse array (you'll get 0 when the element is not present). Alternatively, you can get a dictionary of all (possibly) non-zero elements. MATLAB only supports double, complex, and logical sparse arrays, so `T` here can be `Double`, `Complex` or `Boolean` (which, of course, uses `false` as the default value).
Since `ISparseArrayOf<T>` implements `IArrayOf<T>`, you still can access all the
elements in a sparse array (you'll get 0 when the element is not present).
Alternatively, you can get a dictionary of all (possibly) non-zero elements.
MATLAB only supports double, complex, and logical sparse arrays, so `T` here
can be `Double`, `Complex` or `Boolean` (which, of course, uses `false` as
the default value).
### Object arrays
Matlab objects are similar to structures in that they have some data associated
with fields. As an example, consider a simple `Point` class defined in Matlab as
```matlab
classdef Point
properties
x
y
end
end
```
We omit any methods (and constructors) such a class might have, because they are
not stored when you save an object of a class into a `.mat` file.
Imagine that you have a `1x2 Point` object array `p` (an array of two points)
where the first point has `x=3`, `y=5`, and the second point has `x=-2`, `y=6`.
You can load a mat file containing the variable `p` as usual (using
`MatFileReader`) and access the data using the following interface:
```csharp
public interface IMatObject : IArrayOf<IReadOnlyDictionary<string, IArray>>
{
string ClassName { get; }
IEnumerable<string> FieldNames { get; }
IArray this[string field, params int[] list] { get; set; }
}
```
As you can see, the interface is very similar to `IStructureArray`. The only
addition is the `ClassName` string, which returns the name of object's class
(in our case that would be `Point`). Otherwise, the idea is the same.
In our example, if we load the `.mat` file containing the variable `p` into a
variable named `matFile`, we could then use
```csharp
var matObject = matFile["p"].Value as IMatObject
```
and access the values: `matObject["x", 0] = 3`, `matObject["y", 1] = 6`,
`matObject[1]["x"] = -2`, and so on.
### Tables
Tables in Matlab are just objects of type `table`, so you could use the
interface `IMatObject` described above and get access to all the data in a table
stored in a `.mat` file. However, this is not very convenient, since all the
actual data in a table is stored in one field called `data`, and the
properties are scattered across other fields.
This is why `MatFileHandler` provides a simple wrapper class to work with
tables:
```
public class TableAdapter
{
public TableAdapter(IArray array);
public string Description { get; }
public int NumberOfRows { get; }
public int NumberOfVariables { get; }
public string[] RowNames { get; }
public string[] VariableNames { get; }
public IArray this[string variableName] { get; }
}
```
The constructor creates a `TableAdapter` from an object that you read from a
file. You can access table's description field, query number and names of the
rows and variables of the table, and access all data associated with a single
variable. This accessor returns an array (or a cell array) that has the same
number of rows as table's `NumberOfRows`, and contains values for a given
variable from all the rows (so this is equivalent to Matlab's `t.variable` for
a table `t` having a variable named `variable`).
## Writing a .mat file
After reading a file into `IMatFile matFile`, you can alter some values using the described interfaces, and write the result to a new file:
After reading a file into `IMatFile matFile`, you can alter some values using
the described interfaces, and write the result to a new file:
```csharp
using (var fileStream = new System.IO.FileStream("output.mat", System.IO.FileMode.Create)) {
var writer = new MatFileWriter(fileStream);
writer.Write(matFile);
}
```
By default, all variables are written in a compressed format; you can turn that
off by using another constructor for `MatFileWriter`:
```csharp
var writer = new MatFileWriter(fileStream, new MatFileWriterOptions { UseCompression = CompressionUsage.Never });
```
Another option is to create a file from scratch. You can do it with `DataBuilder` class:
Another option is to create a file from scratch. You can do it with
`DataBuilder` class:
```csharp
public class DataBuilder
@ -145,4 +297,9 @@ public class DataBuilder
public IMatFile NewFile(IEnumerable<IVariable> variables);
}
```
Numerical/logical arrays can be created with `NewArray<T>()` using the provided data; char arrays can be created with `NewCharArray()` using a string. All other types of arrays are created empty. Then you can wrap an array into a variable with `NewVariable()`, and put a bunch of variables into a file using `NewFile()`. The resulting file can be written to a stream using `MatFileWriter`, as shown above.
Numerical/logical arrays can be created with `NewArray<T>()` using the provided
data; char arrays can be created with `NewCharArray()` using a string. All
other types of arrays are created empty. Then you can wrap an array into a
variable with `NewVariable()`, and put a bunch of variables into a file using
`NewFile()`. The resulting file can be written to a stream using
`MatFileWriter`, as shown above.

35
azure-pipelines.yml Normal file
View File

@ -0,0 +1,35 @@
# ASP.NET Core
# Build and test ASP.NET Core projects targeting .NET Core.
# Add steps that run tests, create a NuGet package, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
pool:
vmImage: 'windows-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 9.x'
inputs:
version: 9.x
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'dotnet build $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'dotnet test $(buildConfiguration)'
inputs:
command: test
projects: '**/*Tests/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
configuration: 'release'
- task: PublishBuildArtifacts@1
displayName: 'publish artifacts'

View File

@ -3,7 +3,7 @@
"settings": {
"documentationRules": {
"companyName": "None",
"copyrightText": "Copyright 2017 Alexander Luzgarev",
"copyrightText": "Copyright 2017-2018 Alexander Luzgarev",
"xmlHeader": false
},
"orderingRules": {