Skip to content

Commit c851ee2

Browse files
committed
[feat] support expanding struct to array on the root level
1 parent f03dc01 commit c851ee2

File tree

3 files changed

+104
-18
lines changed

3 files changed

+104
-18
lines changed

jdataencode.m

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@
123123
if (iscell(item))
124124
newitem = cell2jd(item, opt);
125125
elseif (isa(item, 'jdict'))
126-
attrpath = item.getattr();
127-
opt.annotatearray = ~isempty(attrpath);
128126
newitem = obj2jd(item.v(), opt);
129127
elseif (isstruct(item))
130128
newitem = struct2jd(item, opt);
@@ -149,21 +147,34 @@
149147
end
150148

151149
if (isa(item, 'jdict')) % apply attribute
150+
attrpath = item.getattr();
152151
if (isempty(attrpath))
153152
return
154153
end
155154

156155
newitem = jdict(newitem);
157156

158157
for i = 1:length(attrpath)
158+
val = newitem.(attrpath{i}).v();
159+
if (isempty(val))
160+
continue
161+
end
162+
if (isnumeric(val))
163+
opt.annotatearray = 1;
164+
val = mat2jd(val, opt);
165+
end
166+
if (~(isstruct(val) && isfield(val, opt.N_ArrayType)))
167+
continue
168+
end
159169
attr = item.getattr(attrpath{i});
160170
attrname = keys(attr);
161171
for j = 1:length(attrname)
162172
if (strcmp(attrname{j}, 'dims'))
163-
newitem.(attrpath{i}).(opt.N_ArrayLabel) = attr(attrname{j});
173+
val.(opt.N_ArrayLabel) = attr(attrname{j});
164174
else
165-
newitem.(attrpath{i}).(attrname{j}) = attr(attrname{j});
175+
val.(attrname{j}) = attr(attrname{j});
166176
end
177+
newitem.(attrpath{i}) = val;
167178
end
168179
end
169180
end

jdict.m

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,6 @@
404404
% overloaded assignment operator: handling assignments at arbitrary depths
405405
function obj = subsasgn(obj, idxkey, otherobj)
406406
% overloading the setter function, obj.('key').('subkey')=otherobj
407-
% expanded from rahnema1's sample at https://stackoverflow.com/a/79030223/4271392
408407

409408
% handle curly bracket indexing for setting attributes
410409
oplen = length(idxkey);
@@ -423,7 +422,6 @@
423422
if (iscell(idxkey(oplen).subs) && ~isempty(idxkey(oplen).subs))
424423
attrn = idxkey(oplen).subs{1};
425424
if (ischar(attrn))
426-
% Build the path by navigating through keys
427425
temppath = obj.currentpath__;
428426
for i = 1:oplen - 1
429427
idx = idxkey(i);
@@ -443,7 +441,6 @@
443441
end
444442
end
445443
end
446-
% set attribute on original object with computed path
447444
obj.setattr(temppath, attrn, otherobj);
448445
return
449446
end
@@ -454,7 +451,6 @@
454451
if (oplen >= 2 && strcmp(idxkey(oplen).type, '()'))
455452
if (strcmp(idxkey(oplen - 1).type, '.') && ischar(idxkey(oplen - 1).subs))
456453
dimname = idxkey(oplen - 1).subs;
457-
% build path to the data
458454
temppath = obj.currentpath__;
459455
for i = 1:oplen - 2
460456
idx = idxkey(i);
@@ -474,19 +470,15 @@
474470
end
475471
end
476472
end
477-
% check if dimname is in dims
478473
dims = obj.getattr(temppath, 'dims');
479474
if (~isempty(dims) && iscell(dims))
480475
dimpos = find(strcmp(dims, dimname));
481476
if (~isempty(dimpos) && ~isempty(idxkey(oplen).subs))
482-
% build full indices
483477
nddata = length(dims);
484478
indices = repmat({':'}, 1, nddata);
485479
indices{dimpos(1)} = idxkey(oplen).subs{1};
486-
% perform assignment
487480
subsargs = struct('type', '()', 'subs', {indices});
488481
if (oplen > 2)
489-
% need to assign back through the chain
490482
subidx = idxkey(1:oplen - 2);
491483
tempdata = subsref(obj.data, subidx);
492484
tempdata = subsasgn(tempdata, subsargs, otherobj);
@@ -503,7 +495,6 @@
503495
% Fast path: single-level assignment like jd.key = value
504496
if (oplen == 1 && strcmp(idxkey(1).type, '.') && ischar(idxkey(1).subs))
505497
fieldname = idxkey(1).subs;
506-
% Skip if JSONPath
507498
if (isempty(fieldname) || fieldname(1) ~= char(36))
508499
if (isempty(obj.data))
509500
obj.data = struct();
@@ -513,7 +504,6 @@
513504
obj.data.(fieldname) = otherobj;
514505
return
515506
catch
516-
% Field name invalid for struct, convert to Map
517507
fnames = fieldnames(obj.data);
518508
if (~isempty(fnames))
519509
obj.data = containers.Map(fnames, struct2cell(obj.data), 'UniformValues', 0);
@@ -530,8 +520,46 @@
530520
end
531521
end
532522

523+
% Fast path: single numeric index like jd.(1) = value
524+
if (oplen == 1 && strcmp(idxkey(1).type, '.') && isnumeric(idxkey(1).subs))
525+
newidx = idxkey(1).subs;
526+
if isstruct(obj.data) && isstruct(otherobj)
527+
fnames = fieldnames(obj.data);
528+
if isempty(fnames) || numel(obj.data) == 0
529+
objfnames = fieldnames(otherobj);
530+
if newidx == 1
531+
obj.data = otherobj;
532+
else
533+
for fi = 1:length(objfnames)
534+
obj.data(newidx).(objfnames{fi}) = otherobj.(objfnames{fi});
535+
end
536+
end
537+
else
538+
if newidx > numel(obj.data)
539+
for fi = 1:length(fnames)
540+
obj.data(newidx).(fnames{fi}) = [];
541+
end
542+
end
543+
reordered = struct();
544+
for fi = 1:length(fnames)
545+
if isfield(otherobj, fnames{fi})
546+
reordered.(fnames{fi}) = otherobj.(fnames{fi});
547+
else
548+
reordered.(fnames{fi}) = [];
549+
end
550+
end
551+
obj.data(newidx) = reordered;
552+
end
553+
elseif iscell(obj.data)
554+
obj.data{newidx} = otherobj;
555+
else
556+
obj.data(newidx) = otherobj;
557+
end
558+
return
559+
end
560+
533561
oplen = length(idxkey);
534-
opcell = cell (1, oplen + 1);
562+
opcell = cell(1, oplen + 1);
535563
if (isempty(obj.data))
536564
obj.data = obj.newkey_();
537565
end
@@ -540,6 +568,24 @@
540568
for i = 1:oplen
541569
idx = idxkey(i);
542570
if (strcmp(idx.type, '.'))
571+
% Handle numeric indexing: person.(1), person.(2), etc.
572+
if isnumeric(idx.subs)
573+
newidx = idx.subs;
574+
if isstruct(opcell{i}) && isscalar(newidx) && newidx > numel(opcell{i})
575+
fnames = fieldnames(opcell{i});
576+
for fi = 1:length(fnames)
577+
opcell{i}(newidx).(fnames{fi}) = [];
578+
end
579+
elseif iscell(opcell{i}) && isscalar(newidx) && newidx > numel(opcell{i})
580+
opcell{i}{newidx} = [];
581+
end
582+
if iscell(opcell{i})
583+
opcell{i + 1} = opcell{i}{newidx};
584+
else
585+
opcell{i + 1} = opcell{i}(newidx);
586+
end
587+
continue
588+
end
543589
if (ischar(idx.subs) && strcmp(idx.subs, 'v') && i < oplen && strcmp(idxkey(i + 1).type, '()'))
544590
opcell{i + 1} = opcell{i};
545591
if (i < oplen && iscell(opcell{i}))
@@ -548,7 +594,6 @@
548594
continue
549595
end
550596
if (ischar(idx.subs) && ~(~isempty(idx.subs) && idx.subs(1) == char(36)))
551-
% Handle empty or non-struct/map data
552597
if isempty(opcell{i}) || (~isstruct(opcell{i}) && ~ismap_(obj.flags__, opcell{i}))
553598
opcell{i} = obj.newkey_();
554599
end
@@ -604,6 +649,17 @@
604649
idx.type = '()';
605650
end
606651

652+
% Handle numeric indexing in backward loop
653+
if (strcmp(idx.type, '.') && isnumeric(idx.subs))
654+
newidx = idx.subs;
655+
if iscell(opcell{i})
656+
opcell{i}{newidx} = opcell{i + 1};
657+
else
658+
opcell{i}(newidx) = opcell{i + 1};
659+
end
660+
continue
661+
end
662+
607663
if (ischar(idx.subs) && strcmp(idx.subs, 'v') && i < oplen && ismember(idxkey(i + 1).type, {'()', '{}'}))
608664
opcell{i} = opcell{i + 1};
609665
continue
@@ -615,7 +671,6 @@
615671
else
616672
opcell{i} = opcell{i + 1};
617673
end
618-
i = i - 1;
619674
elseif (ischar(idx.subs) && ~isempty(idx.subs) && idx.subs(1) == char(36))
620675
opcell{i} = obj.call_('jsonpath', opcell{i}, idx.subs, opcell{i + 1});
621676
else

test/run_jsonlab_test.m

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,27 @@ function run_jsonlab_test(tests)
497497
test_jsonlab('jd.(''$.key1'').subkey1', @savejson, jd.('key1').('subkey1'), '{"newkey":1}', 'compact', 1);
498498
test_jsonlab('jd.(''$.key1.subkey1'').newkey', @savejson, jd.('key1').('subkey1').newkey, '[1]', 'compact', 1);
499499

500-
clear testdata jd;
500+
% Test basic struct assignment to empty struct array
501+
person = jdict(struct('name', {}, 'age', {}, 'gender', {}));
502+
person.(1) = struct('name', 'Jar Jar', 'age', 100, 'gender', 'M');
503+
test_jsonlab('person.(1) = struct(...)', @savejson, person, '{"name":"Jar Jar","age":100,"gender":"M"}', 'compact', 1);
504+
505+
% Test append second struct
506+
person.(2) = struct('name', 'Jane', 'age', 25, 'gender', 'F');
507+
test_jsonlab('person.(2) = struct(...)', @savejson, person, '[{"name":"Jar Jar","age":100,"gender":"M"},{"name":"Jane","age":25,"gender":"F"}]', 'compact', 1);
508+
509+
% Test field-by-field assignment to new index
510+
person.(3).name = 'Bob';
511+
person.(3).age = 40;
512+
person.(3).gender = 'M';
513+
test_jsonlab('person.(3).name/age/gender', @savejson, person.v(3), '{"name":"Bob","age":40,"gender":"M"}', 'compact', 1);
514+
515+
% Test modify existing element
516+
person.(1).name = 'Jar Jar Modified';
517+
person.(1).age = 101;
518+
test_jsonlab('person.(1).name = ...', @savejson, person.v(1), '{"name":"Jar Jar Modified","age":101,"gender":"M"}', 'compact', 1);
519+
520+
clear testdata jd person;
501521
end
502522

503523
%%

0 commit comments

Comments
 (0)