Skip to content

Commit 13ae6f0

Browse files
HanSur94claude
andcommitted
refactor: update doc format to use per-field .name/.datum structure
The external system's .doc field contains one sub-field per sensor, each with .name (display name) and .datum (datenum field name). - Extract shared extractDatenumField helper to private/ - Update loadModuleData and loadModuleMetadata to use new format - Update all tests to match new .doc structure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 083b1b0 commit 13ae6f0

5 files changed

Lines changed: 100 additions & 75 deletions

File tree

libs/SensorThreshold/loadModuleData.m

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
%LOADMODULEDATA Match module struct fields to registered sensors and assign X/Y.
33
% sensors = loadModuleData(registry, moduleStruct) takes an
44
% ExternalSensorRegistry and a module struct loaded from the external
5-
% system. The struct must contain a .doc.date field naming the datenum
6-
% field. Each struct field whose name matches a registered sensor key
7-
% gets its data assigned as sensor.Y, with the shared datenum as
8-
% sensor.X.
5+
% system. The struct must contain a .doc field where each sub-field has
6+
% .name and .datum properties. The .datum value names the shared
7+
% datenum field. Each struct field whose name matches a registered
8+
% sensor key gets its data assigned as sensor.Y, with the shared
9+
% datenum as sensor.X.
910
%
1011
% Returns a 1xN cell array of filled Sensor handles (empty 1x0 if no
1112
% matches). Output order follows fieldnames(moduleStruct).
@@ -17,27 +18,8 @@
1718

1819
narginchk(2, 2);
1920

20-
% --- Validate doc metadata ---
21-
if ~isfield(moduleStruct, 'doc')
22-
error('loadModuleData:missingDoc', ...
23-
'Module struct must contain a ''doc'' field.');
24-
end
25-
if ~isfield(moduleStruct.doc, 'date')
26-
error('loadModuleData:missingDocDate', ...
27-
'Module struct .doc must contain a ''date'' field naming the datenum variable.');
28-
end
29-
30-
datenumField = moduleStruct.doc.date;
31-
32-
if ~ischar(datenumField)
33-
error('loadModuleData:invalidDocDate', ...
34-
'Module struct .doc.date must be a char (field name), got %s.', class(datenumField));
35-
end
36-
37-
if ~isfield(moduleStruct, datenumField)
38-
error('loadModuleData:missingDatenum', ...
39-
'Datenum field ''%s'' (from doc.date) not found in module struct.', datenumField);
40-
end
21+
% --- Extract datenum field name from doc metadata ---
22+
datenumField = extractDatenumField(moduleStruct, 'loadModuleData');
4123

4224
% --- Extract shared time vector ---
4325
X = moduleStruct.(datenumField);

libs/SensorThreshold/loadModuleMetadata.m

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
% whose ThresholdRules reference matching state keys.
77
%
88
% metadataStruct must have the same format as module data: fields +
9-
% doc.date naming the datenum field. State signals can be numeric
10-
% arrays or cell arrays of char.
9+
% .doc with per-field .name/.datum entries. The .datum value names the
10+
% shared datenum field. State signals can be numeric arrays or cell
11+
% arrays of char.
1112
%
1213
% ThresholdRules must be attached to sensors before calling this
1314
% function. Sensors with no rules are skipped. Rules with empty
@@ -24,29 +25,8 @@
2425

2526
narginchk(2, 2);
2627

27-
% --- Validate doc metadata (same pattern as loadModuleData) ---
28-
if ~isfield(metadataStruct, 'doc')
29-
error('loadModuleMetadata:missingDoc', ...
30-
'Metadata struct must contain a ''doc'' field.');
31-
end
32-
if ~isfield(metadataStruct.doc, 'date')
33-
error('loadModuleMetadata:missingDocDate', ...
34-
'Metadata struct .doc must contain a ''date'' field naming the datenum variable.');
35-
end
36-
37-
datenumField = metadataStruct.doc.date;
38-
39-
if ~ischar(datenumField)
40-
error('loadModuleMetadata:invalidDocDate', ...
41-
'Metadata struct .doc.date must be a char (field name), got %s.', ...
42-
class(datenumField));
43-
end
44-
45-
if ~isfield(metadataStruct, datenumField)
46-
error('loadModuleMetadata:missingDatenum', ...
47-
'Datenum field ''%s'' (from doc.date) not found in metadata struct.', ...
48-
datenumField);
49-
end
28+
% --- Extract datenum field name from doc metadata ---
29+
datenumField = extractDatenumField(metadataStruct, 'loadModuleMetadata');
5030

5131
% --- Early exit for empty sensors ---
5232
if isempty(sensors)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
function datenumField = extractDatenumField(moduleStruct, callerName)
2+
%EXTRACTDATENUMFIELD Extract the datenum field name from a module struct.
3+
% datenumField = extractDatenumField(moduleStruct, callerName) reads the
4+
% .doc metadata to find the shared datenum field name. The .doc struct
5+
% contains one sub-field per data field, each with .name and .datum
6+
% properties. The .datum value names the datenum field.
7+
%
8+
% callerName is used in error identifiers for clear diagnostics.
9+
%
10+
% See also loadModuleData, loadModuleMetadata.
11+
12+
if ~isfield(moduleStruct, 'doc')
13+
error([callerName ':missingDoc'], ...
14+
'Module struct must contain a ''doc'' field.');
15+
end
16+
17+
doc = moduleStruct.doc;
18+
docFields = fieldnames(doc);
19+
20+
if isempty(docFields)
21+
error([callerName ':emptyDoc'], ...
22+
'Module struct .doc has no fields.');
23+
end
24+
25+
% Read .datum from the first doc entry
26+
firstEntry = doc.(docFields{1});
27+
28+
if ~isstruct(firstEntry) || ~isfield(firstEntry, 'datum')
29+
error([callerName ':missingDatum'], ...
30+
'Module struct .doc.%s must be a struct with a ''datum'' field.', ...
31+
docFields{1});
32+
end
33+
34+
datenumField = firstEntry.datum;
35+
36+
if ~ischar(datenumField)
37+
error([callerName ':invalidDatum'], ...
38+
'Module struct .doc.%s.datum must be a char (field name), got %s.', ...
39+
docFields{1}, class(datenumField));
40+
end
41+
42+
if ~isfield(moduleStruct, datenumField)
43+
error([callerName ':missingDatenum'], ...
44+
'Datenum field ''%s'' (from doc.%s.datum) not found in module struct.', ...
45+
datenumField, docFields{1});
46+
end
47+
end

tests/suite/TestLoadModuleData.m

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ function addPaths(testCase)
1010
methods (Static)
1111
function ms = makeModuleStruct(sensorKeys, nPoints)
1212
%MAKEMODULESTRUCT Build a fake module struct for testing.
13-
ms.doc.date = 'time_utc';
1413
ms.time_utc = linspace(datenum(2024,1,1), datenum(2024,1,2), nPoints);
1514
for i = 1:numel(sensorKeys)
1615
ms.(sensorKeys{i}) = randn(1, nPoints);
16+
ms.doc.(sensorKeys{i}).name = sensorKeys{i};
17+
ms.doc.(sensorKeys{i}).datum = 'time_utc';
1718
end
1819
end
1920
end
@@ -102,8 +103,8 @@ function testDocFieldExcluded(testCase)
102103
reg.register('doc', Sensor('doc'));
103104
reg.register('temp', Sensor('temp'));
104105

105-
% Manually build struct so 'doc' is a data field that also has .date
106-
ms.doc.date = 'time_utc';
106+
ms.doc.temp.name = 'Temperature';
107+
ms.doc.temp.datum = 'time_utc';
107108
ms.time_utc = linspace(datenum(2024,1,1), datenum(2024,1,2), 50);
108109
ms.temp = randn(1, 50);
109110
sensors = loadModuleData(reg, ms);
@@ -137,21 +138,25 @@ function testMissingDocFieldErrors(testCase)
137138
testCase.verifyTrue(threw, 'missing_doc_throws');
138139
end
139140

140-
function testMissingDocDateErrors(testCase)
141+
function testDocMissingDatumErrors(testCase)
142+
% doc entry without .datum field
141143
reg = ExternalSensorRegistry('Test');
142-
ms = struct('doc', struct('version', '1.0'), 'temp', [1 2 3]);
144+
ms.doc.temp.name = 'Temperature'; % no .datum
145+
ms.temp = [1 2 3];
143146
threw = false;
144147
try
145148
loadModuleData(reg, ms);
146149
catch
147150
threw = true;
148151
end
149-
testCase.verifyTrue(threw, 'missing_doc_date_throws');
152+
testCase.verifyTrue(threw, 'missing_datum_throws');
150153
end
151154

152155
function testDatenumFieldNotInStructErrors(testCase)
153156
reg = ExternalSensorRegistry('Test');
154-
ms = struct('doc', struct('date', 'nonexistent'), 'temp', [1 2 3]);
157+
ms.doc.temp.name = 'Temperature';
158+
ms.doc.temp.datum = 'nonexistent';
159+
ms.temp = [1 2 3];
155160
threw = false;
156161
try
157162
loadModuleData(reg, ms);
@@ -161,17 +166,19 @@ function testDatenumFieldNotInStructErrors(testCase)
161166
testCase.verifyTrue(threw, 'bad_datenum_ref_throws');
162167
end
163168

164-
function testDocDateNotCharErrors(testCase)
165-
% Defensive test beyond spec scope: validates doc.date type
169+
function testDatumNotCharErrors(testCase)
170+
% Defensive test: validates datum type
166171
reg = ExternalSensorRegistry('Test');
167-
ms = struct('doc', struct('date', 42), 'temp', [1 2 3]);
172+
ms.doc.temp.name = 'Temperature';
173+
ms.doc.temp.datum = 42;
174+
ms.temp = [1 2 3];
168175
threw = false;
169176
try
170177
loadModuleData(reg, ms);
171178
catch
172179
threw = true;
173180
end
174-
testCase.verifyTrue(threw, 'non_char_date_throws');
181+
testCase.verifyTrue(threw, 'non_char_datum_throws');
175182
end
176183

177184
function testOutputIsRowCell(testCase)

tests/suite/TestLoadModuleMetadata.m

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ function addPaths(testCase)
1010
methods (Static)
1111
function ms = makeMetadataStruct(stateKeys, nPoints)
1212
%MAKEMETADATASTRUCT Build a fake metadata struct for testing.
13-
ms.doc.date = 'time_utc';
1413
ms.time_utc = linspace(datenum(2024,1,1), datenum(2024,1,2), nPoints);
1514
for i = 1:numel(stateKeys)
1615
ms.(stateKeys{i}) = zeros(1, nPoints);
16+
ms.doc.(stateKeys{i}).name = stateKeys{i};
17+
ms.doc.(stateKeys{i}).datum = 'time_utc';
1718
end
1819
end
1920

@@ -53,7 +54,8 @@ function testCellStringState(testCase)
5354
s = TestLoadModuleMetadata.makeSensorWithRule( ...
5455
'temp', struct('recipe', 'bake'), 80);
5556

56-
ms.doc.date = 'time_utc';
57+
ms.doc.recipe.name = 'recipe';
58+
ms.doc.recipe.datum = 'time_utc';
5759
ms.time_utc = linspace(datenum(2024,1,1), datenum(2024,1,2), 6);
5860
ms.recipe = {'idle', 'idle', 'bake', 'bake', 'bake', 'idle'};
5961

@@ -156,7 +158,8 @@ function testSinglePointMetadata(testCase)
156158
s = TestLoadModuleMetadata.makeSensorWithRule( ...
157159
'temp', struct('machine', 1), 50);
158160

159-
ms.doc.date = 'time_utc';
161+
ms.doc.machine.name = 'machine';
162+
ms.doc.machine.datum = 'time_utc';
160163
ms.time_utc = datenum(2024,1,1);
161164
ms.machine = 1;
162165

@@ -172,7 +175,8 @@ function testColumnVectorInputs(testCase)
172175
s = TestLoadModuleMetadata.makeSensorWithRule( ...
173176
'temp', struct('machine', 1), 50);
174177

175-
ms.doc.date = 'time_utc';
178+
ms.doc.machine.name = 'machine';
179+
ms.doc.machine.datum = 'time_utc';
176180
ms.time_utc = linspace(datenum(2024,1,1), datenum(2024,1,2), 6)';
177181
ms.machine = [0; 0; 1; 1; 0; 0];
178182

@@ -200,20 +204,23 @@ function testMissingDocErrors(testCase)
200204
testCase.verifyTrue(threw, 'missing_doc_throws');
201205
end
202206

203-
function testMissingDocDateErrors(testCase)
204-
ms = struct('doc', struct('version', '1.0'), 'machine', [1 2 3]);
207+
function testDocMissingDatumErrors(testCase)
208+
% doc entry without .datum field
209+
ms.doc.machine.name = 'machine'; % no .datum
210+
ms.machine = [1 2 3];
205211
threw = false;
206212
try
207213
loadModuleMetadata(ms, {});
208214
catch
209215
threw = true;
210216
end
211-
testCase.verifyTrue(threw, 'missing_doc_date_throws');
217+
testCase.verifyTrue(threw, 'missing_datum_throws');
212218
end
213219

214-
function testDocDateNotInStructErrors(testCase)
215-
ms = struct('doc', struct('date', 'nonexistent'), ...
216-
'machine', [1 2 3]);
220+
function testDatenumFieldNotInStructErrors(testCase)
221+
ms.doc.machine.name = 'machine';
222+
ms.doc.machine.datum = 'nonexistent';
223+
ms.machine = [1 2 3];
217224
threw = false;
218225
try
219226
loadModuleMetadata(ms, {});
@@ -223,16 +230,18 @@ function testDocDateNotInStructErrors(testCase)
223230
testCase.verifyTrue(threw, 'bad_datenum_ref_throws');
224231
end
225232

226-
function testDocDateNotCharErrors(testCase)
227-
% Defensive test beyond spec scope
228-
ms = struct('doc', struct('date', 42), 'machine', [1 2 3]);
233+
function testDatumNotCharErrors(testCase)
234+
% Defensive test: validates datum type
235+
ms.doc.machine.name = 'machine';
236+
ms.doc.machine.datum = 42;
237+
ms.machine = [1 2 3];
229238
threw = false;
230239
try
231240
loadModuleMetadata(ms, {});
232241
catch
233242
threw = true;
234243
end
235-
testCase.verifyTrue(threw, 'non_char_date_throws');
244+
testCase.verifyTrue(threw, 'non_char_datum_throws');
236245
end
237246

238247
function testOutputRowOrientation(testCase)

0 commit comments

Comments
 (0)