diff --git a/+bids/+internal/file_utils.m b/+bids/+internal/file_utils.m index fab6f83e..d4ed3bf3 100644 --- a/+bids/+internal/file_utils.m +++ b/+bids/+internal/file_utils.m @@ -1,234 +1,237 @@ -function varargout = file_utils(str,varargin) -% Character array (or cell array of strings) handling facility -% FORMAT str = bids.internal.file_utils(str,option) -% str - character array, or cell array of strings -% option - string of requested item - one among: -% {'path', 'basename', 'ext', 'filename', 'cpath', 'fpath'} -% -% FORMAT str = bids.internal.file_utils(str,opt_key,opt_val,...) -% str - character array, or cell array of strings -% opt_key - string of targeted item - one among: -% {'path', 'basename', 'ext', 'filename', 'prefix', 'suffix'} -% opt_val - string of new value for feature -%__________________________________________________________________________ -% -% Based on spm_file.m and spm_select.m from SPM12. -%__________________________________________________________________________ +function varargout = file_utils(str, varargin) + % Character array (or cell array of strings) handling facility + % FORMAT str = bids.internal.file_utils(str,option) + % str - character array, or cell array of strings + % option - string of requested item - one among: + % {'path', 'basename', 'ext', 'filename', 'cpath', 'fpath'} + % + % FORMAT str = bids.internal.file_utils(str,opt_key,opt_val,...) + % str - character array, or cell array of strings + % opt_key - string of targeted item - one among: + % {'path', 'basename', 'ext', 'filename', 'prefix', 'suffix'} + % opt_val - string of new value for feature + % __________________________________________________________________________ + % + % Based on spm_file.m and spm_select.m from SPM12. + % __________________________________________________________________________ -% Copyright (C) 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging -%#ok<*AGROW> + %#ok<*AGROW> -if ismember(lower(str), {'list','fplist'}) + if ismember(lower(str), {'list', 'fplist'}) [varargout{1:nargout}] = listfiles(str, varargin{:}); - return; -end + return + end -needchar = ischar(str); -options = varargin; + needchar = ischar(str); + options = varargin; -str = cellstr(str); + str = cellstr(str); -%-Get item -%========================================================================== -if numel(options) == 1 - for n=1:numel(str) - [pth,nam,ext] = fileparts(deblank(str{n})); - switch lower(options{1}) - case 'path' - str{n} = pth; - case 'basename' - str{n} = nam; - case 'ext' - str{n} = ext(2:end); - case 'filename' - str{n} = [nam ext]; - case 'cpath' - str(n) = cpath(str(n)); - case 'fpath' - str{n} = fileparts(char(cpath(str(n)))); - otherwise - error('Unknown option: ''%s''',options{1}); - end + % -Get item + % ========================================================================== + if numel(options) == 1 + for n = 1:numel(str) + [pth, nam, ext] = fileparts(deblank(str{n})); + switch lower(options{1}) + case 'path' + str{n} = pth; + case 'basename' + str{n} = nam; + case 'ext' + str{n} = ext(2:end); + case 'filename' + str{n} = [nam ext]; + case 'cpath' + str(n) = cpath(str(n)); + case 'fpath' + str{n} = fileparts(char(cpath(str(n)))); + otherwise + error('Unknown option: ''%s''', options{1}); + end end options = {}; -end + end -%-Set item -%========================================================================== -while ~isempty(options) - for n=1:numel(str) - [pth,nam,ext] = fileparts(deblank(str{n})); - switch lower(options{1}) - case 'path' - pth = char(options{2}); - case 'basename' - nam = char(options{2}); - case 'ext' - ext = char(options{2}); - if ~isempty(ext) && ext(1) ~= '.' - ext = ['.' ext]; - end - case 'filename' - nam = char(options{2}); - ext = ''; - case 'prefix' - nam = [char(options{2}) nam]; - case 'suffix' - nam = [nam char(options{2})]; - otherwise - warning('Unknown item ''%s'': ignored.',lower(options{1})); - end - str{n} = fullfile(pth,[nam ext]); + % -Set item + % ========================================================================== + while ~isempty(options) + for n = 1:numel(str) + [pth, nam, ext] = fileparts(deblank(str{n})); + switch lower(options{1}) + case 'path' + pth = char(options{2}); + case 'basename' + nam = char(options{2}); + case 'ext' + ext = char(options{2}); + if ~isempty(ext) && ext(1) ~= '.' + ext = ['.' ext]; + end + case 'filename' + nam = char(options{2}); + ext = ''; + case 'prefix' + nam = [char(options{2}) nam]; + case 'suffix' + nam = [nam char(options{2})]; + otherwise + warning('Unknown item ''%s'': ignored.', lower(options{1})); + end + str{n} = fullfile(pth, [nam ext]); end options([1 2]) = []; -end + end -if needchar + if needchar str = char(str); -end -varargout = {str}; + end + varargout = {str}; - -%========================================================================== -%-Canonicalise paths to full path names -%========================================================================== -function t = cpath(t,d) -% canonicalise paths to full path names, removing xxx/./yyy and xxx/../yyy -% constructs -% t must be a cell array of (relative or absolute) paths, d must be a -% single cell containing the base path of relative paths in t -if ispc % valid absolute paths + % ========================================================================== + % -Canonicalise paths to full path names + % ========================================================================== +function t = cpath(t, d) + % canonicalise paths to full path names, removing xxx/./yyy and xxx/../yyy + % constructs + % t must be a cell array of (relative or absolute) paths, d must be a + % single cell containing the base path of relative paths in t + if ispc % valid absolute paths % Allow drive letter or UNC path mch = '^([a-zA-Z]:)|(\\\\[^\\]*)'; -else + else mch = '^/'; -end -if (nargin<2)||isempty(d), d = {pwd}; end -% Find partial paths, prepend them with d -ppsel = cellfun(@isempty, regexp(t,mch,'once')); -t(ppsel) = cellfun(@(t1)fullfile(d{1},t1),t(ppsel),'UniformOutput',false); -% Break paths into cell lists of folder names -pt = pathparts(t); -% Remove single '.' folder names -sd = cellfun(@(pt1)strcmp(pt1,'.'),pt,'UniformOutput',false); -for cp = 1:numel(pt) + end + if (nargin < 2) || isempty(d) + d = {pwd}; + end + % Find partial paths, prepend them with d + ppsel = cellfun(@isempty, regexp(t, mch, 'once')); + t(ppsel) = cellfun(@(t1)fullfile(d{1}, t1), t(ppsel), 'UniformOutput', false); + % Break paths into cell lists of folder names + pt = pathparts(t); + % Remove single '.' folder names + sd = cellfun(@(pt1)strcmp(pt1, '.'), pt, 'UniformOutput', false); + for cp = 1:numel(pt) pt{cp} = pt{cp}(~sd{cp}); -end -% Go up one level for '..' folders, don't remove drive letter/server name -% from PC path -if ispc + end + % Go up one level for '..' folders, don't remove drive letter/server name + % from PC path + if ispc ptstart = 2; -else + else ptstart = 1; -end -for cp = 1:numel(pt) + end + for cp = 1:numel(pt) tmppt = {}; for cdir = ptstart:numel(pt{cp}) - if strcmp(pt{cp}{cdir},'..') - tmppt = tmppt(1:end-1); - else - tmppt{end+1} = pt{cp}{cdir}; - end + if strcmp(pt{cp}{cdir}, '..') + tmppt = tmppt(1:end - 1); + else + tmppt{end + 1} = pt{cp}{cdir}; + end end if ispc - pt{cp} = [pt{cp}(1) tmppt]; + pt{cp} = [pt{cp}(1) tmppt]; else - pt{cp} = tmppt; + pt{cp} = tmppt; end -end -% Assemble paths -if ispc - t = cellfun(@(pt1)fullfile(pt1{:}),pt,'UniformOutput',false); -else - t = cellfun(@(pt1)fullfile(filesep,pt1{:}),pt,'UniformOutput',false); -end - + end + % Assemble paths + if ispc + t = cellfun(@(pt1)fullfile(pt1{:}), pt, 'UniformOutput', false); + else + t = cellfun(@(pt1)fullfile(filesep, pt1{:}), pt, 'UniformOutput', false); + end -%========================================================================== -%-Parse paths -%========================================================================== + % ========================================================================== + % -Parse paths + % ========================================================================== function pp = pathparts(p) -% parse paths in cellstr p -% returns cell array of path component cellstr arrays -% For PC (WIN) targets, both '\' and '/' are accepted as filesep, similar -% to MATLAB fileparts -if ispc + % parse paths in cellstr p + % returns cell array of path component cellstr arrays + % For PC (WIN) targets, both '\' and '/' are accepted as filesep, similar + % to MATLAB fileparts + if ispc fs = '\\/'; -else + else fs = filesep; -end -pp = cellfun(@(p1)textscan(p1,'%s','delimiter',fs,'MultipleDelimsAsOne',1),p); -if ispc + end + pp = cellfun(@(p1)textscan(p1, '%s', 'delimiter', fs, 'MultipleDelimsAsOne', 1), p); + if ispc for k = 1:numel(pp) - if ~isempty(regexp(pp{k}{1}, '^[a-zA-Z]:$', 'once')) - pp{k}{1} = strcat(pp{k}{1}, filesep); - elseif ~isempty(regexp(p{k}, '^\\\\', 'once')) - pp{k}{1} = strcat(filesep, filesep, pp{k}{1}); - end + if ~isempty(regexp(pp{k}{1}, '^[a-zA-Z]:$', 'once')) + pp{k}{1} = strcat(pp{k}{1}, filesep); + elseif ~isempty(regexp(p{k}, '^\\\\', 'once')) + pp{k}{1} = strcat(filesep, filesep, pp{k}{1}); + end end -end - + end -%========================================================================== -%-List files and directories -%========================================================================== -function [fi, di] = listfiles(action,d,varargin) -% FORMAT [files, dirs] = listfiles('List',dir,regexp) -% FORMAT [files, dirs] = listfiles('FPList',dir,regexp) -% FORMAT [dirs] = listfiles('List',dir,'dir',regexp) -% FORMAT [dirs] = listfiles('FPList',dir,'dir',regexp) + % ========================================================================== + % -List files and directories + % ========================================================================== +function [fi, di] = listfiles(action, d, varargin) + % FORMAT [files, dirs] = listfiles('List',dir,regexp) + % FORMAT [files, dirs] = listfiles('FPList',dir,regexp) + % FORMAT [dirs] = listfiles('List',dir,'dir',regexp) + % FORMAT [dirs] = listfiles('FPList',dir,'dir',regexp) -fi = ''; -di = ''; -switch lower(action) + fi = ''; + di = ''; + switch lower(action) case 'list' - fp = false; + fp = false; case 'fplist' - fp = true; + fp = true; otherwise - error('Invalid action: ''%s''.',action); -end -if nargin < 2 + error('Invalid action: ''%s''.', action); + end + if nargin < 2 d = pwd; -else - d = bids.internal.file_utils(d,'cpath'); -end -dirmode = false; -if nargin < 3 + else + d = bids.internal.file_utils(d, 'cpath'); + end + dirmode = false; + if nargin < 3 expr = '.*'; -else - if strcmpi(varargin{1},'dir') - dirmode = true; - if nargin < 4 - expr = '.*'; - else - expr = varargin{2}; - end + else + if strcmpi(varargin{1}, 'dir') + dirmode = true; + if nargin < 4 + expr = '.*'; + else + expr = varargin{2}; + end else - expr = varargin{1}; + expr = varargin{1}; end -end -dd = dir(d); -if isempty(dd) - return; -end -fi = sort({dd(~[dd.isdir]).name})'; -di = sort({dd([dd.isdir]).name})'; -di = setdiff(di,{'.','..'}); -if dirmode - t = regexp(di,expr); - if numel(di)==1 && ~iscell(t), t = {t}; end - di = di(~cellfun(@isempty,t)); + end + dd = dir(d); + if isempty(dd) + return + end + fi = sort({dd(~[dd.isdir]).name})'; + di = sort({dd([dd.isdir]).name})'; + di = setdiff(di, {'.', '..'}); + if dirmode + t = regexp(di, expr); + if numel(di) == 1 && ~iscell(t) + t = {t}; + end + di = di(~cellfun(@isempty, t)); fi = di; -else - t = regexp(fi,expr); - if numel(fi)==1 && ~iscell(t), t = {t}; end - fi = fi(~cellfun(@isempty,t)); -end -if fp - fi = cellfun(@(x) fullfile(d,x), fi, 'UniformOutput',false); - di = cellfun(@(x) fullfile(d,x), di, 'UniformOutput',false); -end -fi = char(fi); -di = char(di); + else + t = regexp(fi, expr); + if numel(fi) == 1 && ~iscell(t) + t = {t}; + end + fi = fi(~cellfun(@isempty, t)); + end + if fp + fi = cellfun(@(x) fullfile(d, x), fi, 'UniformOutput', false); + di = cellfun(@(x) fullfile(d, x), di, 'UniformOutput', false); + end + fi = char(fi); + di = char(di); diff --git a/+bids/+internal/get_metadata.m b/+bids/+internal/get_metadata.m index a1bd6ddc..612658f1 100644 --- a/+bids/+internal/get_metadata.m +++ b/+bids/+internal/get_metadata.m @@ -1,86 +1,90 @@ function meta = get_metadata(filename, pattern) -% Read a BIDS's file metadata according to the inheritance principle -% FORMAT meta = bids.internal.get_metadata(filename, pattern) -% filename - name of file following BIDS standard -% pattern - regular expression matching metadata file -% meta - metadata structure -%__________________________________________________________________________ + % Read a BIDS's file metadata according to the inheritance principle + % FORMAT meta = bids.internal.get_metadata(filename, pattern) + % filename - name of file following BIDS standard + % pattern - regular expression matching metadata file + % meta - metadata structure + % __________________________________________________________________________ -% Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging -% Copyright (C) 2018--, BIDS-MATLAB developers + % Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers + if nargin == 1 + pattern = '^.*%s\\.json$'; + end -if nargin == 1, pattern = '^.*%s\\.json$'; end + pth = fileparts(filename); + p = bids.internal.parse_filename(filename); -pth = fileparts(filename); -p = bids.internal.parse_filename(filename); + meta = struct(); -meta = struct(); + N = 3; -N = 3; + % -There is a session level in the hierarchy + if isfield(p, 'ses') && ~isempty(p.ses) + N = N + 1; + end -%-There is a session level in the hierarchy -if isfield(p,'ses') && ~isempty(p.ses) - N = N + 1; -end - -%-Loop from the directory where the file of interest is back to the -% top level of the BIDS hierarchy -for n=1:N - - %-List the potential metadata files associated with this file suffix type + % -Loop from the directory where the file of interest is back to the + % top level of the BIDS hierarchy + for n = 1:N + + % -List the potential metadata files associated with this file suffix type % Default is to assume it is a JSON file metafile = bids.internal.file_utils('FPList', pth, sprintf(pattern, p.type)); - - if isempty(metafile), metafile = {}; else metafile = cellstr(metafile); end - - %-For all those files we find which one is potentially associated with + + if isempty(metafile) + metafile = {}; + else + metafile = cellstr(metafile); + end + + % -For all those files we find which one is potentially associated with % the file of interest - for i=1:numel(metafile) - - p2 = bids.internal.parse_filename(metafile{i}); - fn = setdiff(fieldnames(p2),{'filename','ext','type'}); - - %-Check if this metadata file contains the same entity-label pairs as its - % data file counterpart - ismeta = true; - for j=1:numel(fn) - if ~isfield(p,fn{j}) || ~strcmp(p.(fn{j}),p2.(fn{j})) - ismeta = false; - break; - end + for i = 1:numel(metafile) + + p2 = bids.internal.parse_filename(metafile{i}); + fn = setdiff(fieldnames(p2), {'filename', 'ext', 'type'}); + + % -Check if this metadata file contains the same entity-label pairs as its + % data file counterpart + ismeta = true; + for j = 1:numel(fn) + if ~isfield(p, fn{j}) || ~strcmp(p.(fn{j}), p2.(fn{j})) + ismeta = false; + break end - - %-Read the content of the metadata file if it is a JSON file and update - % the metadata concerning the file of interest otherwise store the filename - if ismeta - if strcmp(p2.ext,'.json') - meta = update_metadata(meta,bids.util.jsondecode(metafile{i}),metafile{i}); - else - meta.filename = metafile{i}; - end + end + + % -Read the content of the metadata file if it is a JSON file and update + % the metadata concerning the file of interest otherwise store the filename + if ismeta + if strcmp(p2.ext, '.json') + meta = update_metadata(meta, bids.util.jsondecode(metafile{i}), metafile{i}); + else + meta.filename = metafile{i}; end - + end + end - - %-Go up to the parent folder - pth = fullfile(pth,'..'); - -end - - -%========================================================================== -%-Inheritance principle -%========================================================================== -function s1 = update_metadata(s1,s2,file) -if isempty(s2) - return; -elseif ~isstruct(s2) - error('Metadata file contents were neither struct nor empty. File: %s',file); -end -fn = fieldnames(s2); -for i=1:numel(fn) - if ~isfield(s1,fn{i}) - s1.(fn{i}) = s2.(fn{i}); + + % -Go up to the parent folder + pth = fullfile(pth, '..'); + + end + + % ========================================================================== + % -Inheritance principle + % ========================================================================== +function s1 = update_metadata(s1, s2, file) + if isempty(s2) + return + elseif ~isstruct(s2) + error('Metadata file contents were neither struct nor empty. File: %s', file); + end + fn = fieldnames(s2); + for i = 1:numel(fn) + if ~isfield(s1, fn{i}) + s1.(fn{i}) = s2.(fn{i}); end -end + end diff --git a/+bids/+internal/parse_filename.m b/+bids/+internal/parse_filename.m index 9e972588..681a2c8e 100644 --- a/+bids/+internal/parse_filename.m +++ b/+bids/+internal/parse_filename.m @@ -1,56 +1,56 @@ -function p = parse_filename(filename,fields) -% Split a filename into its building constituents -% FORMAT p = bids.internal.parse_filename(filename,fields) -% -% Example: -% -% >> filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; -% >> bids.internal.parse_filename(filename) -% -% ans = -% -% struct with fields: -% -% filename: 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz' -% type: 'FLASH' -% ext: '.nii.gz' -% sub: '16' -% ses: 'mri' -% run: '1' -% echo: '2' -%__________________________________________________________________________ +function p = parse_filename(filename, fields) + % Split a filename into its building constituents + % FORMAT p = bids.internal.parse_filename(filename, fields) + % + % Example: + % + % >> filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + % >> bids.internal.parse_filename(filename) + % + % ans = + % + % struct with fields: + % + % filename: 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz' + % type: 'FLASH' + % ext: '.nii.gz' + % sub: '16' + % ses: 'mri' + % run: '1' + % echo: '2' + % __________________________________________________________________________ -% Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging -% Copyright (C) 2018--, BIDS-MATLAB developers + % Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers -filename = bids.internal.file_utils(filename,'filename'); + filename = bids.internal.file_utils(filename, 'filename'); -%-Identify all the BIDS entity-label pairs present in the filename (delimited by "_") -% https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html -[parts, dummy] = regexp(filename,'(?:_)+','split','match'); %#ok -p.filename = filename; + % -Identify all the BIDS entity-label pairs present in the filename (delimited by "_") + % https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html + [parts, dummy] = regexp(filename, '(?:_)+', 'split', 'match'); %#ok + p.filename = filename; -%-Identify the suffix and extension of this file -% https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#file-name-structure -[p.type, p.ext] = strtok(parts{end},'.'); + % -Identify the suffix and extension of this file + % https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#file-name-structure + [p.type, p.ext] = strtok(parts{end}, '.'); -%-Separate the entity from the label for each pair identified above -for i=1:numel(parts)-1 - [d, dummy] = regexp(parts{i},'(?:\-)+','split','match'); %#ok + % -Separate the entity from the label for each pair identified above + for i = 1:numel(parts) - 1 + [d, dummy] = regexp(parts{i}, '(?:\-)+', 'split', 'match'); %#ok p.(d{1}) = d{2}; -end + end -%-Extra fields can be added to the structure and ordered specifically. -if nargin == 2 - for i=1:numel(fields) - if ~isfield(p,fields{i}) - p.(fields{i}) = ''; - end + % -Extra fields can be added to the structure and ordered specifically. + if nargin == 2 + for i = 1:numel(fields) + if ~isfield(p, fields{i}) + p.(fields{i}) = ''; + end end try - p = orderfields(p,['filename','ext','type',fields]); + p = orderfields(p, ['filename', 'ext', 'type', fields]); catch - warning('Ignoring file ''%s'' not matching template.',filename); - p = struct([]); + warning('Ignoring file ''%s'' not matching template.', filename); + p = struct([]); end -end + end diff --git a/+bids/+util/jsondecode.m b/+bids/+util/jsondecode.m index 67b915d8..624c0285 100644 --- a/+bids/+util/jsondecode.m +++ b/+bids/+util/jsondecode.m @@ -1,30 +1,31 @@ function value = jsondecode(file, varargin) -% Decode JSON-formatted file -% FORMAT value = bids.util.jsondecode(file, opts) -% file - name of a JSON file or JSON string -% opts - structure of optional parameters (only with JSONio): -% replacementStyle: string to control how non-alphanumeric -% characters are replaced {'underscore','hex','delete','nop'} -% [Default: 'underscore'] -% -% json - JSON structure + % Decode JSON-formatted file + % FORMAT value = bids.util.jsondecode(file, opts) + % file - name of a JSON file or JSON string + % opts - structure of optional parameters (only with JSONio): + % replacementStyle: string to control how non-alphanumeric + % characters are replaced {'underscore','hex','delete','nop'} + % [Default: 'underscore'] + % + % json - JSON structure -% Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging -% Copyright (C) 2018--, BIDS-MATLAB developers + % Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers -persistent has_jsondecode -if isempty(has_jsondecode) + persistent has_jsondecode + if isempty(has_jsondecode) has_jsondecode = ... - exist('jsondecode','builtin') == 5 || ... % MATLAB >= R2016b - ismember(exist('jsondecode','file'), [2 3]); % jsonstuff or other Matlab-compatible implementation -end + exist('jsondecode', 'builtin') == 5 || ... % MATLAB >= R2016b + ismember(exist('jsondecode', 'file'), [2 3]); % jsonstuff / Matlab-compatible implementation + end -if has_jsondecode + if has_jsondecode value = jsondecode(fileread(file)); -elseif exist('spm_jsonread','file') == 3 % SPM12 + elseif exist('spm_jsonread', 'file') == 3 % SPM12 value = spm_jsonread(file, varargin{:}); -elseif exist('jsonread','file') == 3 % JSONio + elseif exist('jsonread', 'file') == 3 % JSONio value = jsonread(file, varargin{:}); -else - error('JSON library required: install JSONio from https://github.com/gllmflndn/JSONio'); -end + else + url = 'https://github.com/gllmflndn/JSONio'; + error('JSON library required: install JSONio from: %s', url); + end diff --git a/+bids/+util/jsonencode.m b/+bids/+util/jsonencode.m index ed3b823e..2b85c602 100644 --- a/+bids/+util/jsonencode.m +++ b/+bids/+util/jsonencode.m @@ -1,65 +1,66 @@ function varargout = jsonencode(varargin) -% Encode data to JSON-formatted file -% FORMAT bids.util.jsonencode(filename,json) -% filename - JSON filename -% json - JSON structure -% -% FORMAT S = bids.util.jsonencode(json) -% json - JSON structure -% S - serialized JSON structure (string) -% -% FORMAT [...] = bids.util.jsonencode(...,opts) -% opts - structure of optional parameters: -% Indent: string to use for indentation [Default: ''] -% ReplacementStyle: string to control how non-alphanumeric -% characters are replaced [Default: 'underscore'] -% ConvertInfAndNaN: encode NaN, Inf and -Inf as "null" -% [Default: true] + % Encode data to JSON-formatted file + % FORMAT bids.util.jsonencode(filename,json) + % filename - JSON filename + % json - JSON structure + % + % FORMAT S = bids.util.jsonencode(json) + % json - JSON structure + % S - serialized JSON structure (string) + % + % FORMAT [...] = bids.util.jsonencode(...,opts) + % opts - structure of optional parameters: + % Indent: string to use for indentation [Default: ''] + % ReplacementStyle: string to control how non-alphanumeric + % characters are replaced [Default: 'underscore'] + % ConvertInfAndNaN: encode NaN, Inf and -Inf as "null" + % [Default: true] -% Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging -% Copyright (C) 2018--, BIDS-MATLAB developers + % Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers -if ~nargin + if ~nargin error('Not enough input arguments.'); -end + end -persistent has_jsonencode -if isempty(has_jsonencode) + persistent has_jsonencode + if isempty(has_jsonencode) has_jsonencode = ... - exist('jsonencode','builtin') == 5 || ... % MATLAB >= R2016b - ismember(exist('jsonencode','file'), [2 3]); % jsonstuff or other Matlab-compatible implementation -end + exist('jsonencode', 'builtin') == 5 || ... % MATLAB >= R2016b + ismember(exist('jsonencode', 'file'), [2 3]); % jsonstuff / Matlab-compatible implementation + end -if exist('spm_jsonwrite','file') == 2 % SPM12 + if exist('spm_jsonwrite', 'file') == 2 % SPM12 [varargout{1:nargout}] = spm_jsonwrite(varargin{:}); -elseif exist('jsonwrite','file') == 2 % JSONio + elseif exist('jsonwrite', 'file') == 2 % JSONio [varargout{1:nargout}] = jsonwrite(varargin{:}); -elseif has_jsonencode + elseif has_jsonencode file = ''; if ischar(varargin{1}) - file = varargin{1}; - varargin(1) = []; + file = varargin{1}; + varargin(1) = []; end if numel(varargin) > 1 - opts = varargin{2}; - varargin(2) = []; - fn = fieldnames(opts); - for i=1:numel(fn) - if strcmpi(fn{i},'ConvertInfAndNaN') - varargin(2:3) = {'ConvertInfAndNaN',opts.(fn{i})}; - end + opts = varargin{2}; + varargin(2) = []; + fn = fieldnames(opts); + for i = 1:numel(fn) + if strcmpi(fn{i}, 'ConvertInfAndNaN') + varargin(2:3) = {'ConvertInfAndNaN', opts.(fn{i})}; end + end end txt = jsonencode(varargin{:}); if ~isempty(file) - fid = fopen(file,'wt'); - if fid == -1 - error('Unable to open file "%s" for writing.',file); - end - fprintf(fid,'%s',txt); - fclose(fid); + fid = fopen(file, 'wt'); + if fid == -1 + error('Unable to open file "%s" for writing.', file); + end + fprintf(fid, '%s', txt); + fclose(fid); end varargout = { txt }; -else - error('JSON library required: install JSONio from https://github.com/gllmflndn/JSONio'); -end + else + url = 'https://github.com/gllmflndn/JSONio'; + error('JSON library required: install JSONio from: %s', url); + end diff --git a/+bids/+util/tsvread.m b/+bids/+util/tsvread.m index f4a726a3..96ce60a7 100644 --- a/+bids/+util/tsvread.m +++ b/+bids/+util/tsvread.m @@ -1,208 +1,210 @@ function fileContent = tsvread(filename, fieldToReturn, hdr) - % Load text and numeric data from tab-separated-value or other file - % FORMAT x = tsvread(f,v,hdr) - % f - filename (can be gzipped) {txt,mat,csv,tsv,json} - % v - name of field to return if data stored in a structure [default: ''] - % or index of column if data stored as an array - % hdr - detect the presence of a header row for csv/tsv [default: true] - % - % x - corresponding data array or structure + % Load text and numeric data from tab-separated-value or other file + % FORMAT x = tsvread(f,v,hdr) + % f - filename (can be gzipped) {txt,mat,csv,tsv,json} + % v - name of field to return if data stored in a structure [default: ''] + % or index of column if data stored as an array + % hdr - detect the presence of a header row for csv/tsv [default: true] + % + % x - corresponding data array or structure - % __________________________________________________________________________ - % - % Based on spm_load.m from SPM12. - % __________________________________________________________________________ + % __________________________________________________________________________ + % + % Based on spm_load.m from SPM12. + % __________________________________________________________________________ - % Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging - % Copyright (C) 2018--, BIDS-MATLAB developers + % Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers - % -Check input arguments - % -------------------------------------------------------------------------- - if nargin < 1 - error('no input file specified'); - end - if ~exist(filename, 'file') - error('Unable to read file ''%s'': file not found', filename); - end + % -Check input arguments + % -------------------------------------------------------------------------- + if nargin < 1 + error('no input file specified'); + end + if ~exist(filename, 'file') + error('Unable to read file ''%s'': file not found', filename); + end - if nargin < 2 - fieldToReturn = ''; - end - if nargin < 3 - hdr = true; - end % Detect + if nargin < 2 + fieldToReturn = ''; + end + if nargin < 3 + hdr = true; + end % Detect - % -Load the data file - % -------------------------------------------------------------------------- - [~, ~, ext] = fileparts(filename); - switch ext(2:end) - case 'txt' - fileContent = load(filename, '-ascii'); - case 'mat' - fileContent = load(filename, '-mat'); - case 'csv' - % x = csvread(f); % numeric data only - fileContent = dsv_read(filename, ',', hdr); - case 'tsv' - % x = dlmread(f,'\t'); % numeric data only - fileContent = dsv_read(filename, '\t', hdr); - case 'json' - fileContent = bids.util.jsondecode(filename); - case 'gz' - fz = gunzip(filename, tempname); - sts = true; - try - fileContent = bids.util.tsvread(fz{1}); - catch err - sts = false; - err_msg = err.message; - end - delete(fz{1}); - rmdir(fileparts(fz{1})); - if ~sts - error('Cannot load file ''%s'': %s.', filename, err_msg); - end - otherwise - try - fileContent = load(filename); - catch - error('Cannot read file ''%s'': Unknown file format.', filename); - end - end + % -Load the data file + % -------------------------------------------------------------------------- + [~, ~, ext] = fileparts(filename); + switch ext(2:end) + case 'txt' + fileContent = load(filename, '-ascii'); + case 'mat' + fileContent = load(filename, '-mat'); + case 'csv' + % x = csvread(f); % numeric data only + fileContent = dsv_read(filename, ',', hdr); + case 'tsv' + % x = dlmread(f,'\t'); % numeric data only + fileContent = dsv_read(filename, '\t', hdr); + case 'json' + fileContent = bids.util.jsondecode(filename); + case 'gz' + fz = gunzip(filename, tempname); + sts = true; + try + fileContent = bids.util.tsvread(fz{1}); + catch err + sts = false; + err_msg = err.message; + end + delete(fz{1}); + rmdir(fileparts(fz{1})); + if ~sts + error('Cannot load file ''%s'': %s.', filename, err_msg); + end + otherwise + try + fileContent = load(filename); + catch + error('Cannot read file ''%s'': Unknown file format.', filename); + end + end - % -Return relevant subset of the data if required - % -------------------------------------------------------------------------- - if isstruct(fileContent) - if isempty(fieldToReturn) - fieldsList = fieldnames(fileContent); - if numel(fieldsList) == 1 && isnumeric(fileContent.(fieldsList{1})) - fileContent = fileContent.(fieldsList{1}); - end - else - if ischar(fieldToReturn) - try - fileContent = fileContent.(fieldToReturn); - catch - error('Data do not contain array ''%s''.', fieldToReturn); - end - else - fieldsList = fieldnames(fileContent); - try - fileContent = fileContent.(fieldsList{fieldToReturn}); - catch - error('Data index out of range: %d (data contains %d fields)', ... - fieldToReturn, numel(fieldsList)); - end - end + % -Return relevant subset of the data if required + % -------------------------------------------------------------------------- + if isstruct(fileContent) + if isempty(fieldToReturn) + fieldsList = fieldnames(fileContent); + if numel(fieldsList) == 1 && isnumeric(fileContent.(fieldsList{1})) + fileContent = fileContent.(fieldsList{1}); + end + else + if ischar(fieldToReturn) + try + fileContent = fileContent.(fieldToReturn); + catch + error('Data do not contain array ''%s''.', fieldToReturn); end - elseif isnumeric(fileContent) - if isnumeric(fieldToReturn) - try - fileContent = fileContent(:, fieldToReturn); - catch - error('Data index out of range: %d (data contains $d columns).', ... - fieldToReturn, size(fileContent, 2)); - end - elseif ~isempty(fieldToReturn) - error('Invalid data index. When data is numeric, index must be numeric or empty; got a %s', ... - class(fieldToReturn)); + else + fieldsList = fieldnames(fileContent); + try + fileContent = fileContent.(fieldsList{fieldToReturn}); + catch + error('Data index out of range: %d (data contains %d fields)', ... + fieldToReturn, numel(fieldsList)); end + end + end + elseif isnumeric(fileContent) + if isnumeric(fieldToReturn) + try + fileContent = fileContent(:, fieldToReturn); + catch + error('Data index out of range: %d (data contains $d columns).', ... + fieldToReturn, size(fileContent, 2)); + end + elseif ~isempty(fieldToReturn) + error(['Invalid data index. ', ... + 'When data is numeric, index must be numeric or empty. ', ... + 'Got a %s'], ... + class(fieldToReturn)); end + end - % ========================================================================== - % function x = dsv_read(f,delim) - % ========================================================================== + % ========================================================================== + % function x = dsv_read(f,delim) + % ========================================================================== function x = dsv_read(filename, delim, header) - % Read delimiter-separated values file into a structure array - % * header line of column names will be used if detected - % * 'n/a' fields are replaced with NaN + % Read delimiter-separated values file into a structure array + % * header line of column names will be used if detected + % * 'n/a' fields are replaced with NaN - % -Input arguments - % -------------------------------------------------------------------------- - if nargin < 2 - delim = '\t'; - end - if nargin < 3 - header = true; - end % true: detect, false: no - delim = sprintf(delim); - eol = sprintf('\n'); %#ok + % -Input arguments + % -------------------------------------------------------------------------- + if nargin < 2 + delim = '\t'; + end + if nargin < 3 + header = true; + end % true: detect, false: no + delim = sprintf(delim); + eol = sprintf('\n'); %#ok - % -Read file - % -------------------------------------------------------------------------- - S = fileread(filename); - if isempty(S) - x = []; - return - end - if S(end) ~= eol - S = [S eol]; - end - S = regexprep(S, {'\r\n', '\r', '(\n)\1+'}, {'\n', '\n', '$1'}); + % -Read file + % -------------------------------------------------------------------------- + S = fileread(filename); + if isempty(S) + x = []; + return + end + if S(end) ~= eol + S = [S eol]; + end + S = regexprep(S, {'\r\n', '\r', '(\n)\1+'}, {'\n', '\n', '$1'}); - % -Get column names from header line (non-numeric first line) - % -------------------------------------------------------------------------- - h = find(S == eol, 1); - hdr = S(1:h - 1); - var = regexp(hdr, delim, 'split'); - N = numel(var); - n1 = isnan(cellfun(@str2double, var)); - n2 = cellfun(@(x) strcmpi(x, 'NaN'), var); - if header && any(n1 & ~n2) - hdr = true; - try - var = genvarname(var); %#ok - catch - var = matlab.lang.makeValidName(var, 'ReplacementStyle', 'hex'); - var = matlab.lang.makeUniqueStrings(var); - end - S = S(h + 1:end); - else - hdr = false; - fmt = ['Var%0' num2str(floor(log10(N)) + 1) 'd']; - var = arrayfun(@(x) sprintf(fmt, x), (1:N)', 'UniformOutput', false); + % -Get column names from header line (non-numeric first line) + % -------------------------------------------------------------------------- + h = find(S == eol, 1); + hdr = S(1:h - 1); + var = regexp(hdr, delim, 'split'); + N = numel(var); + n1 = isnan(cellfun(@str2double, var)); + n2 = cellfun(@(x) strcmpi(x, 'NaN'), var); + if header && any(n1 & ~n2) + hdr = true; + try + var = genvarname(var); %#ok + catch + var = matlab.lang.makeValidName(var, 'ReplacementStyle', 'hex'); + var = matlab.lang.makeUniqueStrings(var); end + S = S(h + 1:end); + else + hdr = false; + fmt = ['Var%0' num2str(floor(log10(N)) + 1) 'd']; + var = arrayfun(@(x) sprintf(fmt, x), (1:N)', 'UniformOutput', false); + end - % -Parse file - % -------------------------------------------------------------------------- - if exist('OCTAVE_VERSION', 'builtin') % bug #51093 - S = strrep(S, delim, '#'); - delim = '#'; - end - if ~isempty(S) - d = textscan(S, '%s', 'Delimiter', delim); - else - d = {[]}; - end - if rem(numel(d{1}), N) - error('Invalid DSV file ''%s'': Varying number of delimiters per line.', ... + % -Parse file + % -------------------------------------------------------------------------- + if exist('OCTAVE_VERSION', 'builtin') % bug #51093 + S = strrep(S, delim, '#'); + delim = '#'; + end + if ~isempty(S) + d = textscan(S, '%s', 'Delimiter', delim); + else + d = {[]}; + end + if rem(numel(d{1}), N) + error('Invalid DSV file ''%s'': Varying number of delimiters per line.', ... filename); - end - d = reshape(d{1}, N, [])'; - allnum = true; - for i = 1:numel(var) - sts = true; - dd = zeros(size(d, 1), 1); - for j = 1:size(d, 1) - if strcmp(d{j, i}, 'n/a') - dd(j) = NaN; - else - dd(j) = str2double(d{j, i}); % i,j considered as complex - if isnan(dd(j)) - sts = false; - break - end - end - end - if sts - x.(var{i}) = dd; - else - x.(var{i}) = d(:, i); - allnum = false; + end + d = reshape(d{1}, N, [])'; + allnum = true; + for i = 1:numel(var) + sts = true; + dd = zeros(size(d, 1), 1); + for j = 1:size(d, 1) + if strcmp(d{j, i}, 'n/a') + dd(j) = NaN; + else + dd(j) = str2double(d{j, i}); % i,j considered as complex + if isnan(dd(j)) + sts = false; + break end + end end - - if ~hdr && allnum - x = struct2cell(x); - x = [x{:}]; + if sts + x.(var{i}) = dd; + else + x.(var{i}) = d(:, i); + allnum = false; end + end + + if ~hdr && allnum + x = struct2cell(x); + x = [x{:}]; + end diff --git a/+bids/+util/tsvwrite.m b/+bids/+util/tsvwrite.m index 57682ef5..7afc73ea 100644 --- a/+bids/+util/tsvwrite.m +++ b/+bids/+util/tsvwrite.m @@ -1,101 +1,100 @@ function tsvwrite(f, var) -% Save text and numeric data to .tsv file -% FORMAT tsvwrite(f, var) -% f - filename -% var - data array or structure -% -% Adapted from spm_save.m -%__________________________________________________________________________ -% Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging -% Copyright (C) 2018--, BIDS-MATLAB developers - - -delim = sprintf('\t'); - -% If the input is a MATLAB table, the built-in functionality is used -% otherwise export is performed manually -if isstruct(var) || iscell(var) || isnumeric(var) || islogical(var) - + % Save text and numeric data to .tsv file + % FORMAT tsvwrite(f, var) + % f - filename + % var - data array or structure + % + % Adapted from spm_save.m + % __________________________________________________________________________ + % Copyright (C) 2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers + + delim = sprintf('\t'); + + % If the input is a MATLAB table, the built-in functionality is used + % otherwise export is performed manually + if isstruct(var) || iscell(var) || isnumeric(var) || islogical(var) + % Convert input to a common format we will use for writing % var will be a cell array where the first row is the header and all % following rows contains the values to write if isstruct(var) - - fn = fieldnames(var); - var = struct2cell(var)'; - - for i=1:numel(var) - - if ~ischar(var{i}) - var{i} = var{i}(:); - end - - if ~iscell(var{i}) - var{i} = cellstr(num2str(var{i},16)); - var{i} = strtrim(var{i}); - var{i}(cellfun(@(x) strcmp(x,'NaN'),var{i})) = {'n/a'}; - end - + + fn = fieldnames(var); + var = struct2cell(var)'; + + for i = 1:numel(var) + + if ~ischar(var{i}) + var{i} = var{i}(:); end - - var = [fn'; var]; - - elseif iscell(var) || isnumeric(var) || islogical(var) - - if isnumeric(var) || islogical(var) - var = num2cell(var); + + if ~iscell(var{i}) + var{i} = cellstr(num2str(var{i}, 16)); + var{i} = strtrim(var{i}); + var{i}(cellfun(@(x) strcmp(x, 'NaN'), var{i})) = {'n/a'}; end - - var = cellfun(@(x) num2str(x,16), var, 'UniformOutput',false); - var = strtrim(var); - var(cellfun(@(x) strcmp(x,'NaN'),var)) = {'n/a'}; - + + end + + var = [fn'; var]; + + elseif iscell(var) || isnumeric(var) || islogical(var) + + if isnumeric(var) || islogical(var) + var = num2cell(var); + end + + var = cellfun(@(x) num2str(x, 16), var, 'UniformOutput', false); + var = strtrim(var); + var(cellfun(@(x) strcmp(x, 'NaN'), var)) = {'n/a'}; + end - + % Actually write to file - fid = fopen(f,'Wt'); - + fid = fopen(f, 'Wt'); + if fid == -1 - error('Unble to write file %s.', f); + error('Unble to write file %s.', f); end - - for i=1:size(var,1) - - for j=1:size(var,2) - - to_print = var{i,j}; - - if iscell(to_print) - to_print = to_print{1}; - end - - if isempty(to_print) - to_print = 'n/a'; - - elseif any(to_print == delim) - to_print = ['"' to_print '"']; - - end - - fprintf(fid,'%s', to_print); - - if j < size(var, 2) - fprintf(fid, delim); - end - + + for i = 1:size(var, 1) + + for j = 1:size(var, 2) + + to_print = var{i, j}; + + if iscell(to_print) + to_print = to_print{1}; end - - fprintf(fid,'\n'); + + if isempty(to_print) + to_print = 'n/a'; + + elseif any(to_print == delim) + to_print = ['"' to_print '"']; + + end + + fprintf(fid, '%s', to_print); + + if j < size(var, 2) + fprintf(fid, delim); + end + + end + + fprintf(fid, '\n'); end - + fclose(fid); - -elseif isa(var,'table') - writetable( var, f, ... - 'FileType', 'text', ... - 'Delimiter', delim); - -else + + elseif isa(var, 'table') + writetable(var, f, ... + 'FileType', 'text', ... + 'Delimiter', delim); + + else error('Unknown data type.'); - -end + + end diff --git a/+bids/Contents.m b/+bids/Contents.m index 1361f753..1f32f26f 100644 --- a/+bids/Contents.m +++ b/+bids/Contents.m @@ -10,13 +10,13 @@ % util.jsonencode - Encode JSON-formatted file % util.tsvread - Load text and numeric data from tab-separated-value file % util.tsvwrite - Save text and numeric data to tab-separated-value file -%__________________________________________________________________________ +% __________________________________________________________________________ % % BIDS (Brain Imaging Data Structure): https://bids.neuroimaging.io/ % The brain imaging data structure, a format for organizing and % describing outputs of neuroimaging experiments. % K. J. Gorgolewski et al, Scientific Data, 2016. -%__________________________________________________________________________ +% __________________________________________________________________________ % % BIDS-MATLAB is a library that aims at centralising MATLAB/Octave tools % for interacting with datasets conforming to the BIDS format. diff --git a/+bids/layout.m b/+bids/layout.m index e9fb497d..49e2918f 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -1,673 +1,750 @@ -function BIDS = layout(root,tolerant) - % Parse a directory structure formated according to the BIDS standard - % FORMAT BIDS = bids.layout(root) - % root - directory formated according to BIDS [Default: pwd] - % tolerant - if set to 0 (default) only files g - % BIDS - structure containing the BIDS file layout - %__________________________________________________________________________ - % - % BIDS (Brain Imaging Data Structure): https://bids.neuroimaging.io/ - % The brain imaging data structure, a format for organizing and - % describing outputs of neuroimaging experiments. - % K. J. Gorgolewski et al, Scientific Data, 2016. - %__________________________________________________________________________ - - % Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging - % Copyright (C) 2018--, BIDS-MATLAB developers - - - - %-Validate input arguments - %========================================================================== - if ~nargin - root = pwd; - elseif nargin == 1 - if ischar(root) - root = bids.internal.file_utils(root, 'CPath'); - elseif isstruct(root) - BIDS = root; % or BIDS = bids.layout(root.root); - return; - else - error('Invalid syntax.'); - end - elseif nargin > 2 - error('Too many input arguments.'); - end - - if ~exist('tolerant','var') - tolerant = false; +function BIDS = layout(root, tolerant) + % Parse a directory structure formated according to the BIDS standard + % FORMAT BIDS = bids.layout(root) + % root - directory formated according to BIDS [Default: pwd] + % tolerant - if set to 0 (default) only files g + % BIDS - structure containing the BIDS file layout + % __________________________________________________________________________ + % + % BIDS (Brain Imaging Data Structure): https://bids.neuroimaging.io/ + % The brain imaging data structure, a format for organizing and + % describing outputs of neuroimaging experiments. + % K. J. Gorgolewski et al, Scientific Data, 2016. + % __________________________________________________________________________ + + % Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging + % Copyright (C) 2018--, BIDS-MATLAB developers + + % -Validate input arguments + % ========================================================================== + if ~nargin + root = pwd; + elseif nargin == 1 + if ischar(root) + root = bids.internal.file_utils(root, 'CPath'); + elseif isstruct(root) + BIDS = root; % or BIDS = bids.layout(root.root); + return + else + error('Invalid syntax.'); end - - %-BIDS structure - %========================================================================== - - BIDS = struct(... - 'dir',root, ... % BIDS directory - 'description',struct([]), ... % content of dataset_description.json - 'sessions',{{}},... % cellstr of sessions - 'scans',struct([]),... % content of sub-_scans.tsv (should go within subjects) - 'sess',struct([]),... % content of sub-participants_label>_sessions.tsv (should go within subjects) - 'participants',struct([]),... % content of participants.tsv - 'subjects',struct([])); % structure array of subjects - - - % -Validation of BIDS root directory - % ========================================================================== - if ~exist(BIDS.dir, 'dir') - error('BIDS directory does not exist: ''%s''', BIDS.dir); - - elseif ~exist(fullfile(BIDS.dir, 'dataset_description.json'), 'file') - - msg = sprintf('BIDS directory not valid: missing dataset_description.json: ''%s''', ... - BIDS.dir); - - tolerant_message(tolerant, msg); - + elseif nargin > 2 + error('Too many input arguments.'); + end + + if ~exist('tolerant', 'var') + tolerant = false; + end + + % -BIDS structure + % ========================================================================== + + % BIDS.dir -- BIDS directory + % BIDS.description -- content of dataset_description.json + % BIDS.sessions -- cellstr of sessions + % BIDS.scans -- content of sub-_scans.tsv (should go within subjects) + % BIDS.sess -- content of sub-_sessions.tsv (should go within subjects) + % BIDS.participants -- content of participants.tsv + % BIDS.subjects' -- structure array of subjects + + BIDS = struct( ... + 'dir', root, ... + 'description', struct([]), ... + 'sessions', {{}}, ... + 'scans', struct([]), ... + 'sess', struct([]), ... + 'participants', struct([]), ... + 'subjects', struct([])); + + % -Validation of BIDS root directory + % ========================================================================== + if ~exist(BIDS.dir, 'dir') + error('BIDS directory does not exist: ''%s''', BIDS.dir); + + elseif ~exist(fullfile(BIDS.dir, 'dataset_description.json'), 'file') + + msg = sprintf('BIDS directory not valid: missing dataset_description.json: ''%s''', ... + BIDS.dir); + + tolerant_message(tolerant, msg); + + end + + % -Dataset description + % ========================================================================== + try + BIDS.description = bids.util.jsondecode(fullfile(BIDS.dir, 'dataset_description.json')); + catch err + msg = sprintf('BIDS dataset description could not be read: %s', err.message); + tolerant_message(tolerant, msg); + end + + fields_to_check = {'BIDSVersion', 'Name'}; + for iField = 1:numel(fields_to_check) + + if ~isfield(BIDS.description, fields_to_check{iField}) + msg = sprintf( ... + 'BIDS dataset description not valid: missing %s field.', ... + fields_to_check{iField}); + tolerant_message(tolerant, msg); end - - % -Dataset description - % ========================================================================== + + end + + % -Optional directories + % ========================================================================== + % [code/] + % [derivatives/] + % [stimuli/] + % [sourcedata/] + % [phenotype/] + + % -Scans key file + % ========================================================================== + + % sub-/[ses-/] + % sub-_scans.tsv + + % See also optional README and CHANGES files + + % -Participant key file + % ========================================================================== + p = bids.internal.file_utils('FPList', BIDS.dir, '^participants\.tsv$'); + if ~isempty(p) try - BIDS.description = bids.util.jsondecode(fullfile(BIDS.dir, 'dataset_description.json')); - catch err - msg = sprintf('BIDS dataset description could not be read: %s', err.message); - tolerant_message(tolerant, msg); - end - - fields_to_check = {'BIDSVersion', 'Name'}; - for iField = 1:numel(fields_to_check) - - if ~isfield(BIDS.description, fields_to_check{iField}) - msg = sprintf(... - 'BIDS dataset description not valid: missing %s field.', ... - fields_to_check{iField}); - tolerant_message(tolerant, msg); - end - - end - - % -Optional directories - % ========================================================================== - % [code/] - % [derivatives/] - % [stimuli/] - % [sourcedata/] - % [phenotype/] - - % -Scans key file - % ========================================================================== - - % sub-/[ses-/] - % sub-_scans.tsv - - % See also optional README and CHANGES files - - %-Participant key file - %========================================================================== - p = bids.internal.file_utils('FPList',BIDS.dir,'^participants\.tsv$'); - if ~isempty(p) - try - BIDS.participants = bids.util.tsvread(p); - catch - msg = ['unable to read ' p]; - tolerant_message(tolerant, msg); - end - end - p = bids.internal.file_utils('FPList',BIDS.dir,'^participants\.json$'); - if ~isempty(p) - BIDS.participants.meta = bids.util.jsondecode(p); + BIDS.participants = bids.util.tsvread(p); + catch + msg = ['unable to read ' p]; + tolerant_message(tolerant, msg); end - - % -Sessions file - % ========================================================================== - - % sub-/[ses-/] - % sub-[_ses-]_sessions.tsv - - % -Tasks: JSON files are accessed through metadata - % ========================================================================== - % t = bids.internal.file_utils('FPList',BIDS.dir,... - % '^task-.*_(beh|bold|events|channels|physio|stim|meg)\.(json|tsv)$'); - - % -Subjects - % ========================================================================== - sub = cellstr(bids.internal.file_utils('List', BIDS.dir, 'dir', '^sub-.*$')); - if isequal(sub, {''}) - error('No subjects found in BIDS directory.'); - end - - for iSub = 1:numel(sub) - sess = cellstr(bids.internal.file_utils('List', fullfile(BIDS.dir, sub{iSub}), 'dir', '^ses-.*$')); - for iSess = 1:numel(sess) - if isempty(BIDS.subjects) - BIDS.subjects = parse_subject(BIDS.dir, sub{iSub}, sess{iSess}); - else - BIDS.subjects(end + 1) = parse_subject(BIDS.dir, sub{iSub}, sess{iSess}); - end - end + end + p = bids.internal.file_utils('FPList', BIDS.dir, '^participants\.json$'); + if ~isempty(p) + BIDS.participants.meta = bids.util.jsondecode(p); + end + + % -Sessions file + % ========================================================================== + + % sub-/[ses-/] + % sub-[_ses-]_sessions.tsv + + % -Tasks: JSON files are accessed through metadata + % ========================================================================== + % t = bids.internal.file_utils('FPList',BIDS.dir,... + % '^task-.*_(beh|bold|events|channels|physio|stim|meg)\.(json|tsv)$'); + + % -Subjects + % ========================================================================== + sub = cellstr(bids.internal.file_utils('List', BIDS.dir, 'dir', '^sub-.*$')); + if isequal(sub, {''}) + error('No subjects found in BIDS directory.'); + end + + for iSub = 1:numel(sub) + sess = cellstr(bids.internal.file_utils('List', ... + fullfile(BIDS.dir, sub{iSub}), ... + 'dir', ... + '^ses-.*$')); + for iSess = 1:numel(sess) + if isempty(BIDS.subjects) + BIDS.subjects = parse_subject(BIDS.dir, sub{iSub}, sess{iSess}); + else + BIDS.subjects(end + 1) = parse_subject(BIDS.dir, sub{iSub}, sess{iSess}); + end end - + end + end function tolerant_message(tolerant, msg) - if tolerant - warning(msg); - else - error(msg); - end + if tolerant + warning(msg); + else + error(msg); + end end % ========================================================================== % -Parse a subject's directory % ========================================================================== function subject = parse_subject(pth, subjname, sesname) - % For each modality (anat, func, eeg...) all the files from the - % corresponding directory are listed and their filenames parsed with extra - % BIDS valid entities listed (e.g. 'acq','ce','rec','fa'...). - - subject.name = subjname; % subject name ('sub-') - subject.path = fullfile(pth, subjname, sesname); % full path to subject directory - subject.session = sesname; % session name ('' or 'ses-