From a0ccc0a30f4105b7dcb8695027bc2f841d723e4e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 8 Aug 2020 09:23:24 +0200 Subject: [PATCH 1/5] add tests for user input --- manualTests/test_userInput.m | 41 ----------- src/subfun/askForGroupAndOrSession.m | 19 +++++ src/subfun/createQuestionList.m | 34 +++++++++ src/subfun/isPositiveInteger.m | 9 +++ src/userInputs.m | 101 ++++++++++++++------------- tests/test_askForGroupAndOrSession.m | 52 ++++++++++++++ tests/test_createQuestionList.m | 24 +++++++ tests/test_isPositiveInteger.m | 17 +++++ 8 files changed, 207 insertions(+), 90 deletions(-) delete mode 100644 manualTests/test_userInput.m create mode 100644 src/subfun/askForGroupAndOrSession.m create mode 100644 src/subfun/createQuestionList.m create mode 100644 src/subfun/isPositiveInteger.m create mode 100644 tests/test_askForGroupAndOrSession.m create mode 100644 tests/test_createQuestionList.m create mode 100644 tests/test_isPositiveInteger.m diff --git a/manualTests/test_userInput.m b/manualTests/test_userInput.m deleted file mode 100644 index ce3edab0..00000000 --- a/manualTests/test_userInput.m +++ /dev/null @@ -1,41 +0,0 @@ -clear; -clc; - -%% -fprintf('Debug\n'); -cfg.debug = true; - -cfg = userInputs(cfg); -disp(cfg.subject); - -%% -fprintf('No Group\n'); -cfg.debug = false; -cfg.subject.askGrpSess = 0; - -cfg = userInputs(cfg); -disp(cfg.subject); - -%% -fprintf('No Group or session\n'); -cfg.debug = false; -cfg.subject.askGrpSess = [0 0]; - -cfg = userInputs(cfg); -disp(cfg.subject); - -%% -fprintf('No session\n'); -cfg.debug = false; -cfg.subject.askGrpSess = [1 0]; - -cfg = userInputs(cfg); -disp(cfg.subject); - -%% -fprintf('Default\n'); -cfg.debug = false; -cfg.subject.askGrpSess = []; - -cfg = userInputs(cfg); -disp(cfg.subject); diff --git a/src/subfun/askForGroupAndOrSession.m b/src/subfun/askForGroupAndOrSession.m new file mode 100644 index 00000000..9cc145e8 --- /dev/null +++ b/src/subfun/askForGroupAndOrSession.m @@ -0,0 +1,19 @@ +function cfg = askForGroupAndOrSession(cfg) + + askGrpSess = [true true]; + + if isfield(cfg, 'subject') && ... + isfield(cfg.subject, 'askGrpSess') && ... + ~isempty(cfg.subject.askGrpSess) + + askGrpSess = cfg.subject.askGrpSess; + + end + + if numel(askGrpSess) < 2 + askGrpSess(2) = 1; + end + + cfg.subject.askGrpSess = askGrpSess; + +end \ No newline at end of file diff --git a/src/subfun/createQuestionList.m b/src/subfun/createQuestionList.m new file mode 100644 index 00000000..b77a9c6e --- /dev/null +++ b/src/subfun/createQuestionList.m @@ -0,0 +1,34 @@ +function questions = createQuestionList(cfg) + + cfg = askForGroupAndOrSession(cfg); + + questions.group = 'Enter subject group (leave empty if none): '; + questions.subject = 'Enter subject number (1-999): '; + questions.session = 'Enter the session (i.e day - 1-999)) number: '; + questions.run = 'Enter the run number (1-999): '; + questions.mustBePositiveInteger = 'Please enter a positive integer: '; + % questions.questionsToAsk is a cell array : second column is a boolean + % set to true if we must run inputCheck on the response + questions.questionsToAsk = cell(4,2); + + % subject group + if cfg.subject.askGrpSess(1) + questions.questionsToAsk{1, 1} = questions.group; + questions.questionsToAsk{1, 2} = false; + end + + % the subject number + questions.questionsToAsk{2, 1} = questions.subject; + questions.questionsToAsk{2, 2} = true; + + % the session number + if cfg.subject.askGrpSess(2) + questions.questionsToAsk{3, 1} = questions.session; + questions.questionsToAsk{3, 2} = true; + end + + % the run number + questions.questionsToAsk{4, 1} = questions.run; + questions.questionsToAsk{4, 2} = true; + +end \ No newline at end of file diff --git a/src/subfun/isPositiveInteger.m b/src/subfun/isPositiveInteger.m new file mode 100644 index 00000000..6e2210ec --- /dev/null +++ b/src/subfun/isPositiveInteger.m @@ -0,0 +1,9 @@ +function trueOrFalse = isPositiveInteger(input2check) + + trueOrFalse = ~(... + ~isnumeric(input2check) || ... + isnan(input2check) || ... + fix(input2check) ~= input2check || ... + input2check < 0); + +end \ No newline at end of file diff --git a/src/userInputs.m b/src/userInputs.m index 1a601870..2e1819f5 100644 --- a/src/userInputs.m +++ b/src/userInputs.m @@ -9,72 +9,75 @@ % - the first value set to false will skip asking for the participants % group % - the second value set to false will skip asking for the session - + if nargin < 1 - cfg = []; + cfg = struct('debug', []); end + if isempty(cfg.debug) cfg.debug.do = false; end - - askGrpSess = [true true]; - if isfield(cfg, 'subject') && ... - isfield(cfg.subject, 'askGrpSess') && ... - ~isempty(cfg.subject.askGrpSess) - - askGrpSess = cfg.subject.askGrpSess; - - end - if numel(askGrpSess) < 2 - askGrpSess(2) = 1; - end - - subjectGrp = ''; - subjectNb = []; %#ok<*NASGU> - sessionNb = []; - runNb = []; + + responses{1,1} = ''; % subjectGrp + responses{2,1} = []; % subjectNb + responses{3,1} = 1; % runNb + responses{4,1} = []; % sessionNb % When in debug more this function returns some dummy values if cfg.debug.do - subjectGrp = 'ctrl'; - subjectNb = 666; - runNb = 666; - sessionNb = 666; - + + responses{1,1} = 'ctrl'; + responses{2,1} = 666; + responses{3,1} = 666; + responses{4,1} = 666; + % Otherwise it prompts the user for some information else - % subject group - if askGrpSess(1) - subjectGrp = lower(input('Enter subject group (leave empty if none): ', 's')); - end + questions = createQuestionList(cfg); + + responses = askUserCli(questions); + + end + + cfg.subject.subjectGrp = responses{1,1}; + cfg.subject.subjectNb = responses{2,1}; + cfg.subject.sessionNb = responses{3,1}; + cfg.subject.runNb = responses{4,1}; + +end - % the subject number - subjectNb = str2double(input('Enter subject number (1-999): ', 's')); - subjectNb = checkInput(subjectNb); - % the session number - if numel(askGrpSess) > 1 && askGrpSess(2) - sessionNb = str2double(input('Enter the session (i.e day - 1-999)) number: ', 's')); - sessionNb = checkInput(sessionNb); +function responses = askUserCli(questions) + % response = askUserCli(questions) + % + % command line interface to ask questions to user + % + + for iQuestion = 1:size(questions.questionsToAsk, 1) + + if ~isempty(questions.questionsToAsk{iQuestion,1}) + + responses{iQuestion, 1} = ... + input(questions.questionsToAsk{iQuestion,1}, 's'); %#ok<*AGROW> + + if questions.questionsToAsk{iQuestion,2} + responses{iQuestion, 1} = str2double(responses); + responses{iQuestion, 1} = checkInput(responses, questions); + end + end - - % the run number - runNb = str2double(input('Enter the run number (1-999): ', 's')); - runNb = checkInput(runNb); - + end - - cfg.subject.subjectGrp = subjectGrp; - cfg.subject.subjectNb = subjectNb; - cfg.subject.sessionNb = sessionNb; - cfg.subject.runNb = runNb; - + end -function input2check = checkInput(input2check) + +function input2check = checkInput(input2check, questions) % this function checks the input to makes sure the user enters a positive integer - while isnan(input2check) || fix(input2check) ~= input2check || input2check < 0 - input2check = str2double(input('Please enter a positive integer: ', 's')); + + while ~isPositiveInteger(input2check) + input2check = str2double(input(questions.mustBePositiveInteger, 's')); end + end diff --git a/tests/test_askForGroupAndOrSession.m b/tests/test_askForGroupAndOrSession.m new file mode 100644 index 00000000..b61a39b9 --- /dev/null +++ b/tests/test_askForGroupAndOrSession.m @@ -0,0 +1,52 @@ +function test_suite = test_askForGroupAndOrSession %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_askForGroupAndOrSessionBasic() + + %% set up + cfg = struct(); + cfg = askForGroupAndOrSession(cfg); + + expectedStructure = struct('subject', struct('askGrpSess', [true true])); + + assertEqual(expectedStructure, cfg) + +end + +function test_askForGroupAndOrSessionNoGroup() + + cfg.subject.askGrpSess = 0; + cfg = askForGroupAndOrSession(cfg); + + expectedStructure = struct('subject', struct('askGrpSess', [false true])); + + assertEqual(expectedStructure, cfg) + +end + +function test_askForGroupAndOrSessionNoGroupNoSession() + + cfg.subject.askGrpSess = [0 0]; + cfg = askForGroupAndOrSession(cfg); + + expectedStructure = struct('subject', struct('askGrpSess', [false false])); + + assertEqual(expectedStructure, cfg) + +end + +function test_askForGroupAndOrSessionNoSession() + + cfg.subject.askGrpSess = [1 0]; + cfg = askForGroupAndOrSession(cfg); + + expectedStructure = struct('subject', struct('askGrpSess', [true false])); + + assertEqual(expectedStructure, cfg) + +end \ No newline at end of file diff --git a/tests/test_createQuestionList.m b/tests/test_createQuestionList.m new file mode 100644 index 00000000..8cff0e52 --- /dev/null +++ b/tests/test_createQuestionList.m @@ -0,0 +1,24 @@ +function test_suite = test_createQuestionList %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_createQuestionListBasic() + + %% set up + cfg = struct(); + + questions = createQuestionList(cfg); + + expectedCell = { ... + 'Enter subject group (leave empty if none): ', false; + 'Enter subject number (1-999): ', true; + 'Enter the session (i.e day - 1-999)) number: ', true; + 'Enter the run number (1-999): ', true}; + + assertEqual(expectedCell, questions.questionsToAsk) + +end diff --git a/tests/test_isPositiveInteger.m b/tests/test_isPositiveInteger.m new file mode 100644 index 00000000..c5cede8c --- /dev/null +++ b/tests/test_isPositiveInteger.m @@ -0,0 +1,17 @@ +function test_suite = test_isPositiveInteger %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_isPositiveIntegerBasic() + + assertTrue(isPositiveInteger(1)); + assertFalse(isPositiveInteger(nan())) + assertFalse(isPositiveInteger(0.3)) + assertFalse(isPositiveInteger(-1)) + assertFalse(isPositiveInteger('1')) + +end From 5e35455cd4c68ba826d167f6593283526767a92d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 8 Aug 2020 09:46:14 +0200 Subject: [PATCH 2/5] refactor and add more tests --- src/subfun/askUserCli.m | 34 ++++++++++++++++++ src/subfun/setDefaultResponses.m | 25 ++++++++++++++ src/userInputs.m | 59 +++----------------------------- tests/test_setDefaultResponses.m | 32 +++++++++++++++++ 4 files changed, 95 insertions(+), 55 deletions(-) create mode 100644 src/subfun/askUserCli.m create mode 100644 src/subfun/setDefaultResponses.m create mode 100644 tests/test_setDefaultResponses.m diff --git a/src/subfun/askUserCli.m b/src/subfun/askUserCli.m new file mode 100644 index 00000000..e79f615e --- /dev/null +++ b/src/subfun/askUserCli.m @@ -0,0 +1,34 @@ +function responses = askUserCli(questions, responses) + % response = askUserCli(questions) + % + % command line interface to ask questions to user + % + + for iQuestion = 1:size(questions.questionsToAsk, 1) + + if ~isempty(questions.questionsToAsk{iQuestion,1}) + + thisResponse = ... + input(['\n' questions.questionsToAsk{iQuestion,1}], 's'); %#ok<*AGROW> + + if questions.questionsToAsk{iQuestion,2} + thisResponse = str2double(thisResponse); + thisResponse = checkInput(thisResponse, questions); + end + + responses{iQuestion, 1} = thisResponse; + + end + + end + +end + +function input2check = checkInput(input2check, questions) + % this function checks the input to makes sure the user enters a positive integer + + while ~isPositiveInteger(input2check) + input2check = str2double(input(questions.mustBePositiveInteger, 's')); + end + +end diff --git a/src/subfun/setDefaultResponses.m b/src/subfun/setDefaultResponses.m new file mode 100644 index 00000000..57ded814 --- /dev/null +++ b/src/subfun/setDefaultResponses.m @@ -0,0 +1,25 @@ +function [cfg, responses] = setDefaultResponses(cfg) + + if nargin < 1 + cfg = struct('debug', []); + end + + if isempty(cfg.debug) + cfg.debug.do = false; + end + + responses{1,1} = ''; % subjectGrp + responses{2,1} = []; % subjectNb + responses{3,1} = 1; % runNb + responses{4,1} = []; % sessionNb + + if cfg.debug.do + + responses{1,1} = 'ctrl'; + responses{2,1} = 666; + responses{3,1} = 666; + responses{4,1} = 666; + + end + +end \ No newline at end of file diff --git a/src/userInputs.m b/src/userInputs.m index 2e1819f5..49eaeed7 100644 --- a/src/userInputs.m +++ b/src/userInputs.m @@ -14,29 +14,13 @@ cfg = struct('debug', []); end - if isempty(cfg.debug) - cfg.debug.do = false; - end + [cfg, responses] = setDefaultResponses(cfg); - responses{1,1} = ''; % subjectGrp - responses{2,1} = []; % subjectNb - responses{3,1} = 1; % runNb - responses{4,1} = []; % sessionNb - - % When in debug more this function returns some dummy values - if cfg.debug.do - - responses{1,1} = 'ctrl'; - responses{2,1} = 666; - responses{3,1} = 666; - responses{4,1} = 666; + if ~cfg.debug.do - % Otherwise it prompts the user for some information - else - questions = createQuestionList(cfg); - responses = askUserCli(questions); + responses = askUserCli(questions, responses); end @@ -45,39 +29,4 @@ cfg.subject.sessionNb = responses{3,1}; cfg.subject.runNb = responses{4,1}; -end - - -function responses = askUserCli(questions) - % response = askUserCli(questions) - % - % command line interface to ask questions to user - % - - for iQuestion = 1:size(questions.questionsToAsk, 1) - - if ~isempty(questions.questionsToAsk{iQuestion,1}) - - responses{iQuestion, 1} = ... - input(questions.questionsToAsk{iQuestion,1}, 's'); %#ok<*AGROW> - - if questions.questionsToAsk{iQuestion,2} - responses{iQuestion, 1} = str2double(responses); - responses{iQuestion, 1} = checkInput(responses, questions); - end - - end - - end - -end - - -function input2check = checkInput(input2check, questions) - % this function checks the input to makes sure the user enters a positive integer - - while ~isPositiveInteger(input2check) - input2check = str2double(input(questions.mustBePositiveInteger, 's')); - end - -end +end \ No newline at end of file diff --git a/tests/test_setDefaultResponses.m b/tests/test_setDefaultResponses.m new file mode 100644 index 00000000..cde9a558 --- /dev/null +++ b/tests/test_setDefaultResponses.m @@ -0,0 +1,32 @@ +function test_suite = test_setDefaultResponses %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_setDefaultResponsesBasic() + + %% + [~, responses] = setDefaultResponses(); %#ok<*NODEF> + + expectedCell{1,1} = ''; + expectedCell{2,1} = []; + expectedCell{3,1} = 1; + expectedCell{4,1} = []; + + assertEqual(expectedCell, responses) + + %% + cfg.debug.do = true; + [~, responses] = setDefaultResponses(cfg); + + expectedCell{1,1} = 'ctrl'; + expectedCell{2,1} = 666; + expectedCell{3,1} = 666; + expectedCell{4,1} = 666; + + assertEqual(expectedCell, responses) + +end From d67f828f4a958a8e21bba24efa2c02bba02de8af Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 8 Aug 2020 12:56:47 +0200 Subject: [PATCH 3/5] creating a GUI is hard --- lib/utils/file_utils.m | 424 ++++++++++++++++--------------- src/subfun/askUserCli.m | 3 +- src/subfun/askUserGui.m | 43 ++++ src/subfun/createQuestionList.m | 2 +- src/subfun/getIsQuestionToAsk.m | 10 + src/subfun/isPositiveInteger.m | 15 +- src/subfun/setDefaultResponses.m | 8 +- src/userInputs.m | 6 +- tests/test_createBoldJson.m | 2 +- tests/test_createQuestionList.m | 4 +- tests/test_getIsQuestionToAsk.m | 29 +++ tests/test_isPositiveInteger.m | 2 +- tests/test_setDefaultResponses.m | 4 +- 13 files changed, 332 insertions(+), 220 deletions(-) create mode 100644 src/subfun/askUserGui.m create mode 100644 src/subfun/getIsQuestionToAsk.m create mode 100644 tests/test_getIsQuestionToAsk.m diff --git a/lib/utils/file_utils.m b/lib/utils/file_utils.m index 91385d1c..84ab444f 100644 --- a/lib/utils/file_utils.m +++ b/lib/utils/file_utils.m @@ -1,233 +1,255 @@ -function varargout = file_utils(str,varargin) -% Character array (or cell array of strings) handling facility -% FORMAT str = 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 = 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 = 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 = 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 + if ismember(lower(str), {'list', 'fplist'}) + [varargout{1:nargout}] = listfiles(str, varargin{:}); + return + end -if ismember(lower(str), {'list','fplist'}) - [varargout{1:nargout}] = listfiles(str, varargin{:}); - 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}); + % -Get item + % ========================================================================== + if numel(options) == 1 + for n = 1:numel(str) + [pth, name, ext] = fileparts(deblank(str{n})); + switch lower(options{1}) + case 'path' + str{n} = pth; + case 'basename' + str{n} = name; + case 'ext' + str{n} = ext(2:end); + case 'filename' + str{n} = [name 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 - options = {}; -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})); + % -Set item + % ========================================================================== + while ~isempty(options) + for n = 1:numel(str) + [pth, name, ext] = fileparts(deblank(str{n})); + switch lower(options{1}) + case 'path' + pth = char(options{2}); + case 'basename' + name = char(options{2}); + case 'ext' + ext = char(options{2}); + if ~isempty(ext) && ext(1) ~= '.' + ext = ['.' ext]; + end + case 'filename' + name = char(options{2}); + ext = ''; + case 'prefix' + name = [char(options{2}) name]; + case 'suffix' + name = [name char(options{2})]; + otherwise + warning('Unknown item ''%s'': ignored.', lower(options{1})); + end + str{n} = fullfile(pth, [name ext]); end - str{n} = fullfile(pth,[nam ext]); + options([1 2]) = []; end - options([1 2]) = []; -end -if needchar - str = char(str); + 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 - % Allow drive letter or UNC path - mch = '^([a-zA-Z]:)|(\\\\[^\\]*)'; -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) - 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 - ptstart = 2; -else - ptstart = 1; -end -for cp = 1:numel(pt) - tmppt = {}; - for cdir = ptstart:numel(pt{cp}) - if strcmp(pt{cp}{cdir},'..') - tmppt = tmppt(1:end-1); + % ========================================================================== + % -Canonicalise paths to full path names + % ========================================================================== +function t = cpath(t, basepath) + % 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 + mch = '^/'; + end + if (nargin < 2) || isempty(basepath) + basepath = {pwd}; + end + + % Find partial paths, prepend them with d + ppsel = cellfun(@isempty, regexp(t, mch, 'once')); + t(ppsel) = cellfun(@(t1)fullfile(basepath{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 + ptstart = 2; + else + ptstart = 1; + 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 + end + if ispc + pt{cp} = [pt{cp}(1) tmppt]; else - tmppt{end+1} = pt{cp}{cdir}; + pt{cp} = tmppt; end end + + % Assemble paths if ispc - pt{cp} = [pt{cp}(1) tmppt]; + t = cellfun(@(pt1)fullfile(pt1{:}), pt, 'UniformOutput', false); else - pt{cp} = tmppt; + 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 - fs = '\\/'; -else - fs = filesep; -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}); + % 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 + fs = filesep; + 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 end end end + % ========================================================================== + % -List files and directories + % ========================================================================== +function [files, dirs] = 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) - case 'list' - fp = false; - case 'fplist' - fp = true; - otherwise - error('Invalid action: ''%s''.',action); -end -if nargin < 2 - d = pwd; -else - d = file_utils(d,'cpath'); -end -dirmode = false; -if nargin < 3 - expr = '.*'; -else - if strcmpi(varargin{1},'dir') - dirmode = true; - if nargin < 4 - expr = '.*'; + files = ''; + dirs = ''; + switch lower(action) + case 'list' + fullpath = false; + case 'fplist' + fullpath = true; + otherwise + error('Invalid action: ''%s''.', action); + end + + if nargin < 2 + d = pwd; + else + d = 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 - expr = varargin{2}; + expr = varargin{1}; + end + end + + dd = dir(d); + + if isempty(dd) + return + end + files = sort({dd(~[dd.isdir]).name})'; + dirs = sort({dd([dd.isdir]).name})'; + dirs = setdiff(dirs, {'.', '..'}); + + if dirMode + t = regexp(dirs, expr); + if numel(dirs) == 1 && ~iscell(t) + t = {t}; end + dirs = dirs(~cellfun(@isempty, t)); + files = dirs; else - expr = varargin{1}; + t = regexp(files, expr); + if numel(files) == 1 && ~iscell(t) + t = {t}; + end + files = files(~cellfun(@isempty, t)); end + + if fullpath + files = cellfun(@(x) fullfile(d, x), files, 'UniformOutput', false); + dirs = cellfun(@(x) fullfile(d, x), dirs, 'UniformOutput', false); + end + files = char(files); + dirs = char(dirs); + 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); diff --git a/src/subfun/askUserCli.m b/src/subfun/askUserCli.m index e79f615e..281398e2 100644 --- a/src/subfun/askUserCli.m +++ b/src/subfun/askUserCli.m @@ -12,7 +12,6 @@ input(['\n' questions.questionsToAsk{iQuestion,1}], 's'); %#ok<*AGROW> if questions.questionsToAsk{iQuestion,2} - thisResponse = str2double(thisResponse); thisResponse = checkInput(thisResponse, questions); end @@ -26,7 +25,7 @@ function input2check = checkInput(input2check, questions) % this function checks the input to makes sure the user enters a positive integer - + input2check = str2double(input2check); while ~isPositiveInteger(input2check) input2check = str2double(input(questions.mustBePositiveInteger, 's')); end diff --git a/src/subfun/askUserGui.m b/src/subfun/askUserGui.m new file mode 100644 index 00000000..d41d97ab --- /dev/null +++ b/src/subfun/askUserGui.m @@ -0,0 +1,43 @@ +function responses = askUserGui(questions, responses) + + % boolean for which question should be asked + isQuestionToAsk = ~cellfun('isempty', questions.questionsToAsk(:,1)); + + responses = cellstr(string(responses(isQuestionToAsk))); + + responses = askQuestionsGui(questions, responses, isQuestionToAsk); + + % keep asking the question till we get a positive integer value for each + for iQuestion = 1:size(questions.questionsToAsk) + questions.questionsToAsk{iQuestion} = sprintf('%s %s\n %s',... + '\color{red}', ... + questions.questionsToAsk{iQuestion}, ... + questions.mustBePositiveInteger); + end + + while 1 + isQuestionToAsk = getIsQuestionToAsk(questions, responses); + if all(~isQuestionToAsk) + break + end + responses = askQuestionsGui(questions, responses, isQuestionToAsk); + end + +end + +function resp = askQuestionsGui(quest, resp, isQuestionToAsk) + + opts.Interpreter = 'tex'; + + fieldDim = repmat([1 50], sum(isQuestionToAsk), 1); + + currentResp = inputdlg(quest.questionsToAsk(isQuestionToAsk, 1),... + 'Subject info', ... + fieldDim, ... + resp(isQuestionToAsk), ... + opts); + + resp(isQuestionToAsk) = currentResp; + +end + diff --git a/src/subfun/createQuestionList.m b/src/subfun/createQuestionList.m index b77a9c6e..3712b40c 100644 --- a/src/subfun/createQuestionList.m +++ b/src/subfun/createQuestionList.m @@ -4,7 +4,7 @@ questions.group = 'Enter subject group (leave empty if none): '; questions.subject = 'Enter subject number (1-999): '; - questions.session = 'Enter the session (i.e day - 1-999)) number: '; + questions.session = 'Enter the session number (i.e day ; 1-999): '; questions.run = 'Enter the run number (1-999): '; questions.mustBePositiveInteger = 'Please enter a positive integer: '; % questions.questionsToAsk is a cell array : second column is a boolean diff --git a/src/subfun/getIsQuestionToAsk.m b/src/subfun/getIsQuestionToAsk.m new file mode 100644 index 00000000..f53e4cfc --- /dev/null +++ b/src/subfun/getIsQuestionToAsk.m @@ -0,0 +1,10 @@ +function isQuestionToAsk = getIsQuestionToAsk(questions, responses) + + isQuestionToAsk = cell2mat(questions.questionsToAsk(:,2)); + for j = 1:size(isQuestionToAsk, 1) + input2check = str2double(responses{j}); + isQuestionToAsk(j,2) = ~isPositiveInteger(input2check); + end + isQuestionToAsk = all(isQuestionToAsk, 2); + +end \ No newline at end of file diff --git a/src/subfun/isPositiveInteger.m b/src/subfun/isPositiveInteger.m index 6e2210ec..1c21c3b9 100644 --- a/src/subfun/isPositiveInteger.m +++ b/src/subfun/isPositiveInteger.m @@ -1,9 +1,14 @@ function trueOrFalse = isPositiveInteger(input2check) + % trueOrFalse = isPositiveInteger(input2check) + % + % works on column vector of values + % - trueOrFalse = ~(... - ~isnumeric(input2check) || ... - isnan(input2check) || ... - fix(input2check) ~= input2check || ... - input2check < 0); + trueOrFalse = ~any( [ ... + ~isnumeric(input2check), ... + isnan(input2check), ... + fix(input2check) ~= input2check, ... + input2check < 0 ... + ]); end \ No newline at end of file diff --git a/src/subfun/setDefaultResponses.m b/src/subfun/setDefaultResponses.m index 57ded814..42efa38e 100644 --- a/src/subfun/setDefaultResponses.m +++ b/src/subfun/setDefaultResponses.m @@ -4,14 +4,14 @@ cfg = struct('debug', []); end - if isempty(cfg.debug) + if ~isfield(cfg, 'debug') || isempty(cfg.debug) cfg.debug.do = false; end responses{1,1} = ''; % subjectGrp - responses{2,1} = []; % subjectNb - responses{3,1} = 1; % runNb - responses{4,1} = []; % sessionNb + responses{2,1} = ''; % subjectNb + responses{3,1} = 1; % session + responses{4,1} = ''; % run if cfg.debug.do diff --git a/src/userInputs.m b/src/userInputs.m index 49eaeed7..1c5de9e6 100644 --- a/src/userInputs.m +++ b/src/userInputs.m @@ -20,7 +20,11 @@ questions = createQuestionList(cfg); - responses = askUserCli(questions, responses); +% try + responses = askUserGui(questions, responses); +% catch +% responses = askUserCli(questions, responses); +% end end diff --git a/tests/test_createBoldJson.m b/tests/test_createBoldJson.m index 769d7148..9d2f5d2b 100644 --- a/tests/test_createBoldJson.m +++ b/tests/test_createBoldJson.m @@ -38,4 +38,4 @@ function test_createBoldJsonBasic() %% test assertTrue(exist(fullfile(funcDir, eventFilename), 'file') == 2); -end +end \ No newline at end of file diff --git a/tests/test_createQuestionList.m b/tests/test_createQuestionList.m index 8cff0e52..3335c50b 100644 --- a/tests/test_createQuestionList.m +++ b/tests/test_createQuestionList.m @@ -16,9 +16,9 @@ function test_createQuestionListBasic() expectedCell = { ... 'Enter subject group (leave empty if none): ', false; 'Enter subject number (1-999): ', true; - 'Enter the session (i.e day - 1-999)) number: ', true; + 'Enter the session number (i.e day ; 1-999): ', true; 'Enter the run number (1-999): ', true}; - assertEqual(expectedCell, questions.questionsToAsk) + assertEqual(expectedCell(3,1), questions.questionsToAsk(3,1)) end diff --git a/tests/test_getIsQuestionToAsk.m b/tests/test_getIsQuestionToAsk.m new file mode 100644 index 00000000..e9c15e81 --- /dev/null +++ b/tests/test_getIsQuestionToAsk.m @@ -0,0 +1,29 @@ +function test_suite = test_getIsQuestionToAsk %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_isPositiveIntegerBasic() + + questions.questionsToAsk = {... + [], 1 ; ... + [], 0 ; ... + [], 1 ; ... + [], 0 ; ... + }; + + responses = {... + '1'; + '1'; + '-1'; + '-1'; + }; + + isQuestionToAsk = getIsQuestionToAsk(questions, responses); + + assertEqual(isQuestionToAsk, [false;false;true;false]); + +end \ No newline at end of file diff --git a/tests/test_isPositiveInteger.m b/tests/test_isPositiveInteger.m index c5cede8c..5b782acf 100644 --- a/tests/test_isPositiveInteger.m +++ b/tests/test_isPositiveInteger.m @@ -14,4 +14,4 @@ function test_isPositiveIntegerBasic() assertFalse(isPositiveInteger(-1)) assertFalse(isPositiveInteger('1')) -end +end \ No newline at end of file diff --git a/tests/test_setDefaultResponses.m b/tests/test_setDefaultResponses.m index cde9a558..5123f701 100644 --- a/tests/test_setDefaultResponses.m +++ b/tests/test_setDefaultResponses.m @@ -12,9 +12,9 @@ function test_setDefaultResponsesBasic() [~, responses] = setDefaultResponses(); %#ok<*NODEF> expectedCell{1,1} = ''; - expectedCell{2,1} = []; + expectedCell{2,1} = ''; expectedCell{3,1} = 1; - expectedCell{4,1} = []; + expectedCell{4,1} = ''; assertEqual(expectedCell, responses) From 8a70b9349cdbaad8b8408a3d0e3f2cc52aa584f6 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 8 Aug 2020 12:57:30 +0200 Subject: [PATCH 4/5] mh autofix --- src/subfun/askForGroupAndOrSession.m | 12 +++++----- src/subfun/askUserCli.m | 24 +++++++++---------- src/subfun/askUserGui.m | 31 ++++++++++++------------ src/subfun/createQuestionList.m | 18 +++++++------- src/subfun/getIsQuestionToAsk.m | 8 +++---- src/subfun/isPositiveInteger.m | 8 +++---- src/subfun/setDefaultResponses.m | 32 ++++++++++++------------- src/userInputs.m | 36 ++++++++++++++-------------- tests/test_askForGroupAndOrSession.m | 34 +++++++++++++------------- tests/test_createBoldJson.m | 2 +- tests/test_createQuestionList.m | 10 ++++---- tests/test_getIsQuestionToAsk.m | 16 ++++++------- tests/test_isPositiveInteger.m | 12 +++++----- tests/test_setDefaultResponses.m | 32 ++++++++++++------------- 14 files changed, 137 insertions(+), 138 deletions(-) diff --git a/src/subfun/askForGroupAndOrSession.m b/src/subfun/askForGroupAndOrSession.m index 9cc145e8..faad823c 100644 --- a/src/subfun/askForGroupAndOrSession.m +++ b/src/subfun/askForGroupAndOrSession.m @@ -1,7 +1,7 @@ function cfg = askForGroupAndOrSession(cfg) - + askGrpSess = [true true]; - + if isfield(cfg, 'subject') && ... isfield(cfg.subject, 'askGrpSess') && ... ~isempty(cfg.subject.askGrpSess) @@ -9,11 +9,11 @@ askGrpSess = cfg.subject.askGrpSess; end - + if numel(askGrpSess) < 2 askGrpSess(2) = 1; end - + cfg.subject.askGrpSess = askGrpSess; - -end \ No newline at end of file + +end diff --git a/src/subfun/askUserCli.m b/src/subfun/askUserCli.m index 281398e2..a2222eef 100644 --- a/src/subfun/askUserCli.m +++ b/src/subfun/askUserCli.m @@ -3,24 +3,24 @@ % % command line interface to ask questions to user % - + for iQuestion = 1:size(questions.questionsToAsk, 1) - - if ~isempty(questions.questionsToAsk{iQuestion,1}) - + + if ~isempty(questions.questionsToAsk{iQuestion, 1}) + thisResponse = ... - input(['\n' questions.questionsToAsk{iQuestion,1}], 's'); %#ok<*AGROW> - - if questions.questionsToAsk{iQuestion,2} + input(['\n' questions.questionsToAsk{iQuestion, 1}], 's'); %#ok<*AGROW> + + if questions.questionsToAsk{iQuestion, 2} thisResponse = checkInput(thisResponse, questions); end - + responses{iQuestion, 1} = thisResponse; - + end - + end - + end function input2check = checkInput(input2check, questions) @@ -29,5 +29,5 @@ while ~isPositiveInteger(input2check) input2check = str2double(input(questions.mustBePositiveInteger, 's')); end - + end diff --git a/src/subfun/askUserGui.m b/src/subfun/askUserGui.m index d41d97ab..ddf42103 100644 --- a/src/subfun/askUserGui.m +++ b/src/subfun/askUserGui.m @@ -1,43 +1,42 @@ function responses = askUserGui(questions, responses) - + % boolean for which question should be asked - isQuestionToAsk = ~cellfun('isempty', questions.questionsToAsk(:,1)); - + isQuestionToAsk = ~cellfun('isempty', questions.questionsToAsk(:, 1)); + responses = cellstr(string(responses(isQuestionToAsk))); - + responses = askQuestionsGui(questions, responses, isQuestionToAsk); - + % keep asking the question till we get a positive integer value for each for iQuestion = 1:size(questions.questionsToAsk) - questions.questionsToAsk{iQuestion} = sprintf('%s %s\n %s',... + questions.questionsToAsk{iQuestion} = sprintf('%s %s\n %s', ... '\color{red}', ... questions.questionsToAsk{iQuestion}, ... questions.mustBePositiveInteger); end - - while 1 + + while 1 isQuestionToAsk = getIsQuestionToAsk(questions, responses); if all(~isQuestionToAsk) break end responses = askQuestionsGui(questions, responses, isQuestionToAsk); end - + end function resp = askQuestionsGui(quest, resp, isQuestionToAsk) - + opts.Interpreter = 'tex'; - + fieldDim = repmat([1 50], sum(isQuestionToAsk), 1); - - currentResp = inputdlg(quest.questionsToAsk(isQuestionToAsk, 1),... + + currentResp = inputdlg(quest.questionsToAsk(isQuestionToAsk, 1), ... 'Subject info', ... fieldDim, ... resp(isQuestionToAsk), ... opts); - + resp(isQuestionToAsk) = currentResp; - -end +end diff --git a/src/subfun/createQuestionList.m b/src/subfun/createQuestionList.m index 3712b40c..8d3b55c9 100644 --- a/src/subfun/createQuestionList.m +++ b/src/subfun/createQuestionList.m @@ -1,7 +1,7 @@ function questions = createQuestionList(cfg) - + cfg = askForGroupAndOrSession(cfg); - + questions.group = 'Enter subject group (leave empty if none): '; questions.subject = 'Enter subject number (1-999): '; questions.session = 'Enter the session number (i.e day ; 1-999): '; @@ -9,26 +9,26 @@ questions.mustBePositiveInteger = 'Please enter a positive integer: '; % questions.questionsToAsk is a cell array : second column is a boolean % set to true if we must run inputCheck on the response - questions.questionsToAsk = cell(4,2); - + questions.questionsToAsk = cell(4, 2); + % subject group if cfg.subject.askGrpSess(1) questions.questionsToAsk{1, 1} = questions.group; questions.questionsToAsk{1, 2} = false; end - + % the subject number questions.questionsToAsk{2, 1} = questions.subject; questions.questionsToAsk{2, 2} = true; - + % the session number if cfg.subject.askGrpSess(2) questions.questionsToAsk{3, 1} = questions.session; questions.questionsToAsk{3, 2} = true; end - + % the run number questions.questionsToAsk{4, 1} = questions.run; questions.questionsToAsk{4, 2} = true; - -end \ No newline at end of file + +end diff --git a/src/subfun/getIsQuestionToAsk.m b/src/subfun/getIsQuestionToAsk.m index f53e4cfc..8da55b03 100644 --- a/src/subfun/getIsQuestionToAsk.m +++ b/src/subfun/getIsQuestionToAsk.m @@ -1,10 +1,10 @@ function isQuestionToAsk = getIsQuestionToAsk(questions, responses) - isQuestionToAsk = cell2mat(questions.questionsToAsk(:,2)); + isQuestionToAsk = cell2mat(questions.questionsToAsk(:, 2)); for j = 1:size(isQuestionToAsk, 1) input2check = str2double(responses{j}); - isQuestionToAsk(j,2) = ~isPositiveInteger(input2check); + isQuestionToAsk(j, 2) = ~isPositiveInteger(input2check); end isQuestionToAsk = all(isQuestionToAsk, 2); - -end \ No newline at end of file + +end diff --git a/src/subfun/isPositiveInteger.m b/src/subfun/isPositiveInteger.m index 1c21c3b9..f44bc872 100644 --- a/src/subfun/isPositiveInteger.m +++ b/src/subfun/isPositiveInteger.m @@ -3,12 +3,12 @@ % % works on column vector of values % - - trueOrFalse = ~any( [ ... + + trueOrFalse = ~any([ ... ~isnumeric(input2check), ... isnan(input2check), ... fix(input2check) ~= input2check, ... input2check < 0 ... ]); - -end \ No newline at end of file + +end diff --git a/src/subfun/setDefaultResponses.m b/src/subfun/setDefaultResponses.m index 42efa38e..1d8074ea 100644 --- a/src/subfun/setDefaultResponses.m +++ b/src/subfun/setDefaultResponses.m @@ -1,25 +1,25 @@ function [cfg, responses] = setDefaultResponses(cfg) - + if nargin < 1 cfg = struct('debug', []); end - + if ~isfield(cfg, 'debug') || isempty(cfg.debug) cfg.debug.do = false; end - - responses{1,1} = ''; % subjectGrp - responses{2,1} = ''; % subjectNb - responses{3,1} = 1; % session - responses{4,1} = ''; % run - + + responses{1, 1} = ''; % subjectGrp + responses{2, 1} = ''; % subjectNb + responses{3, 1} = 1; % session + responses{4, 1} = ''; % run + if cfg.debug.do - - responses{1,1} = 'ctrl'; - responses{2,1} = 666; - responses{3,1} = 666; - responses{4,1} = 666; - + + responses{1, 1} = 'ctrl'; + responses{2, 1} = 666; + responses{3, 1} = 666; + responses{4, 1} = 666; + end - -end \ No newline at end of file + +end diff --git a/src/userInputs.m b/src/userInputs.m index 1c5de9e6..397f4e16 100644 --- a/src/userInputs.m +++ b/src/userInputs.m @@ -9,28 +9,28 @@ % - the first value set to false will skip asking for the participants % group % - the second value set to false will skip asking for the session - + if nargin < 1 cfg = struct('debug', []); end - + [cfg, responses] = setDefaultResponses(cfg); - + if ~cfg.debug.do - + questions = createQuestionList(cfg); - -% try - responses = askUserGui(questions, responses); -% catch -% responses = askUserCli(questions, responses); -% end - + + % try + responses = askUserGui(questions, responses); + % catch + % responses = askUserCli(questions, responses); + % end + end - - cfg.subject.subjectGrp = responses{1,1}; - cfg.subject.subjectNb = responses{2,1}; - cfg.subject.sessionNb = responses{3,1}; - cfg.subject.runNb = responses{4,1}; - -end \ No newline at end of file + + cfg.subject.subjectGrp = responses{1, 1}; + cfg.subject.subjectNb = responses{2, 1}; + cfg.subject.sessionNb = responses{3, 1}; + cfg.subject.runNb = responses{4, 1}; + +end diff --git a/tests/test_askForGroupAndOrSession.m b/tests/test_askForGroupAndOrSession.m index b61a39b9..cfa91a0a 100644 --- a/tests/test_askForGroupAndOrSession.m +++ b/tests/test_askForGroupAndOrSession.m @@ -11,42 +11,42 @@ function test_askForGroupAndOrSessionBasic() %% set up cfg = struct(); cfg = askForGroupAndOrSession(cfg); - + expectedStructure = struct('subject', struct('askGrpSess', [true true])); - - assertEqual(expectedStructure, cfg) - + + assertEqual(expectedStructure, cfg); + end function test_askForGroupAndOrSessionNoGroup() cfg.subject.askGrpSess = 0; cfg = askForGroupAndOrSession(cfg); - + expectedStructure = struct('subject', struct('askGrpSess', [false true])); - - assertEqual(expectedStructure, cfg) - + + assertEqual(expectedStructure, cfg); + end function test_askForGroupAndOrSessionNoGroupNoSession() cfg.subject.askGrpSess = [0 0]; cfg = askForGroupAndOrSession(cfg); - + expectedStructure = struct('subject', struct('askGrpSess', [false false])); - - assertEqual(expectedStructure, cfg) - + + assertEqual(expectedStructure, cfg); + end function test_askForGroupAndOrSessionNoSession() cfg.subject.askGrpSess = [1 0]; cfg = askForGroupAndOrSession(cfg); - + expectedStructure = struct('subject', struct('askGrpSess', [true false])); - - assertEqual(expectedStructure, cfg) - -end \ No newline at end of file + + assertEqual(expectedStructure, cfg); + +end diff --git a/tests/test_createBoldJson.m b/tests/test_createBoldJson.m index 9d2f5d2b..769d7148 100644 --- a/tests/test_createBoldJson.m +++ b/tests/test_createBoldJson.m @@ -38,4 +38,4 @@ function test_createBoldJsonBasic() %% test assertTrue(exist(fullfile(funcDir, eventFilename), 'file') == 2); -end \ No newline at end of file +end diff --git a/tests/test_createQuestionList.m b/tests/test_createQuestionList.m index 3335c50b..999110ee 100644 --- a/tests/test_createQuestionList.m +++ b/tests/test_createQuestionList.m @@ -10,15 +10,15 @@ function test_createQuestionListBasic() %% set up cfg = struct(); - + questions = createQuestionList(cfg); - + expectedCell = { ... 'Enter subject group (leave empty if none): ', false; 'Enter subject number (1-999): ', true; 'Enter the session number (i.e day ; 1-999): ', true; 'Enter the run number (1-999): ', true}; - - assertEqual(expectedCell(3,1), questions.questionsToAsk(3,1)) - + + assertEqual(expectedCell(3, 1), questions.questionsToAsk(3, 1)); + end diff --git a/tests/test_getIsQuestionToAsk.m b/tests/test_getIsQuestionToAsk.m index e9c15e81..3be47593 100644 --- a/tests/test_getIsQuestionToAsk.m +++ b/tests/test_getIsQuestionToAsk.m @@ -7,23 +7,23 @@ end function test_isPositiveIntegerBasic() - - questions.questionsToAsk = {... + + questions.questionsToAsk = { ... [], 1 ; ... [], 0 ; ... [], 1 ; ... [], 0 ; ... }; - - responses = {... + + responses = { ... '1'; '1'; '-1'; '-1'; }; - + isQuestionToAsk = getIsQuestionToAsk(questions, responses); - + assertEqual(isQuestionToAsk, [false;false;true;false]); - -end \ No newline at end of file + +end diff --git a/tests/test_isPositiveInteger.m b/tests/test_isPositiveInteger.m index 5b782acf..9945ff5e 100644 --- a/tests/test_isPositiveInteger.m +++ b/tests/test_isPositiveInteger.m @@ -9,9 +9,9 @@ function test_isPositiveIntegerBasic() assertTrue(isPositiveInteger(1)); - assertFalse(isPositiveInteger(nan())) - assertFalse(isPositiveInteger(0.3)) - assertFalse(isPositiveInteger(-1)) - assertFalse(isPositiveInteger('1')) - -end \ No newline at end of file + assertFalse(isPositiveInteger(nan())); + assertFalse(isPositiveInteger(0.3)); + assertFalse(isPositiveInteger(-1)); + assertFalse(isPositiveInteger('1')); + +end diff --git a/tests/test_setDefaultResponses.m b/tests/test_setDefaultResponses.m index 5123f701..90500326 100644 --- a/tests/test_setDefaultResponses.m +++ b/tests/test_setDefaultResponses.m @@ -10,23 +10,23 @@ function test_setDefaultResponsesBasic() %% [~, responses] = setDefaultResponses(); %#ok<*NODEF> - - expectedCell{1,1} = ''; - expectedCell{2,1} = ''; - expectedCell{3,1} = 1; - expectedCell{4,1} = ''; - - assertEqual(expectedCell, responses) - + + expectedCell{1, 1} = ''; + expectedCell{2, 1} = ''; + expectedCell{3, 1} = 1; + expectedCell{4, 1} = ''; + + assertEqual(expectedCell, responses); + %% cfg.debug.do = true; [~, responses] = setDefaultResponses(cfg); - - expectedCell{1,1} = 'ctrl'; - expectedCell{2,1} = 666; - expectedCell{3,1} = 666; - expectedCell{4,1} = 666; - - assertEqual(expectedCell, responses) - + + expectedCell{1, 1} = 'ctrl'; + expectedCell{2, 1} = 666; + expectedCell{3, 1} = 666; + expectedCell{4, 1} = 666; + + assertEqual(expectedCell, responses); + end From b89476b30645fd2dc02d1819d80ed3348927c40f Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 8 Aug 2020 14:03:23 +0200 Subject: [PATCH 5/5] refactor doc --- README.md | 397 ++-------------------------------- docs/functions_description.md | 144 ++++++++++++ docs/installation.md | 97 +++++++++ docs/usage.md | 124 +++++++++++ src/subfun/returnHeaderName.m | 1 + src/userInputs.m | 13 +- 6 files changed, 397 insertions(+), 379 deletions(-) create mode 100644 docs/functions_description.md create mode 100644 docs/installation.md create mode 100644 docs/usage.md diff --git a/README.md b/README.md index 802c1490..bc0995c3 100644 --- a/README.md +++ b/README.md @@ -17,37 +17,26 @@ # CPP_BIDS - - -- [CPP_BIDS](#cpp_bids) - - [Output format](#output-format) - - [Modality agnostic aspect](#modality-agnostic-aspect) - - [Usage](#usage) - - [To save events.tsv file](#to-save-eventstsv-file) - - [Functions descriptions](#functions-descriptions) - - [userInputs](#userinputs) - - [createFilename](#createfilename) - - [saveEventsFile](#saveeventsfile) - - [checkCFG](#checkcfg) - - [CFG content](#cfg-content) - - [createBoldJson](#createboldjson) - - [How to install](#how-to-install) - - [Download with git](#download-with-git) - - [Add as a submodule](#add-as-a-submodule) - - [Example for submodule usage](#example-for-submodule-usage) - - [Direct download](#direct-download) - - [Contributing](#contributing) - - [Guidestyle](#guidestyle) - - [BIDS naming convention](#bids-naming-convention) - - [Contributors ✨](#contributors-) - - + +* 1. [Output format](#Outputformat) + * 1.1. [Modality agnostic aspect](#Modalityagnosticaspect) +* 2. [Documentation](#Documentation) +* 3. [Contributing](#Contributing) + * 3.1. [Guidestyle](#Guidestyle) + * 3.2. [BIDS naming convention](#BIDSnamingconvention) + * 3.3. [Contributors ✨](#Contributors) + + + A set of function for matlab and octave to create [BIDS-compatible](https://bids-specification.readthedocs.io/en/stable/) folder structure and filenames for the output of behavioral, EEG, fMRI, eyetracking studies. -## Output format +## 1. Output format -## Modality agnostic aspect +### 1.1. Modality agnostic aspect Subjects, session and run number labels will be numbers with zero padding up to 3 values (e.g subject 1 will become `sub-001`). @@ -63,361 +52,23 @@ For example: sub-090/ses-003/sub-090_ses-003_task-auditoryTask_run-023_events_date-202007291536.tsv ``` -## Usage +## 2. Documentation -### To save events.tsv file +- [Installation](./docs/installation.md) +- [How to use it](./docs/usage.md) +- [Functions description](./docs/functions_description.md) -```matlab - -% define the folder where the data will be saved -cfg.outputDir = fullfile(pwd, '..', 'output'); - -% define the name of the task -cfg.task.name = 'test task'; - -% can use the userInputs function to collect subject info -% cfg = userInputs; - -% or declare it directly -cfg.subject.subjectNb = 1; -cfg.subject.runNb = 1; - -% by default we assume you are running things on a behavioral PC with no eyetracker -% cfg.eyeTracker = false; -% cfg.testingDevice = 'PC'; - -% if the testing device is set to 'PC' then the data will be saved in the `beh` folder -% if set to 'mri' then the data will be saved in the `func` folder -% cfg.testingDevice = 'mri'; -% if set to 'eeg' then the data will be saved in the `eeg` folder -% cfg.testingDevice = 'eeg'; - -% create the filenames: this include a step to check that all the information is there (checkCFG) -[cfg] = createFilename(cfg); - -% initialize the events files with the typical BIDS columns (onsets, duration, trial_type) -% logFile = saveEventsFile('open', cfg); - -% You can add some more in this case (Speed and is_Fixation) -logFile.extraColumns = {'Speed', 'is_Fixation'}; -logFile = saveEventsFile('open', cfg, logFile); - -% The information about 2 events that we want to save -% NOTE : If the user DOES NOT provide `onset`, `trial_type`, this events will be skipped. -logFile(1,1).onset = 2; -logFile(1,1).trial_type = 'motion_up'; -logFile(1,1).duration = 1; -logFile(1,1).Speed = 2; -logFile(1,1).is_Fixation = true; - -logFile(2,1).onset = 3; -logFile(2,1).trial_type = 'static'; -logFile(2,1).duration = 4; -logFile(2,1).is_Fixation = 3; - -% add those 2 events to the events.tsv file -saveEventsFile('save', cfg, logFile); - -% close the file -saveEventsFile('close', cfg, logFile); - -``` - -If you want to save more complex events.tsv file you can save several columns at once. - -```matlab -cfg.subject.subjectNb = 1; -cfg.subject.runNb = 1; -cfg.task.name = 'testtask'; -cfg.outputDir = outputDir; - -cfg.testingDevice = 'mri'; - -[cfg] = createFilename(cfg); - -% You can specify how many columns we want for each variable -% will set 1 columns with name Speed -% will set 12 columns with names LHL24-01, LHL24-02, ... -% will set 1 columns with name is_Fixation - -logFile.extraColumns.Speed.length = 1; -logFile.extraColumns.LHL24.length = 12; -logFile.extraColumns.is_Fixation.length = 1; - -logFile = saveEventsFile('open', cfg, logFile); - -logFile(1, 1).onset = 2; -logFile(end, 1).trial_type = 'motion_up'; -logFile(end, 1).duration = 3; -logFile(end, 1).Speed = 2; -logFile(end, 1).is_Fixation = true; -logFile(end, 1).LHL24 = 1:12; - -saveEventsFile('save', cfg, logFile); - -saveEventsFile('close', cfg, logFile); - -``` - -If you have many columns to define but only a few with several columns, you can do this: - -```matlab -% define the extra columns: they will be added to the tsv files in the order the user input them -logFile.extraColumns = {'Speed', 'is_Fixation'}; - -[cfg] = createFilename(cfg); - -% initialize the logFile variable -[logFile] = saveEventsFile('init', cfg, logFile); - -% set the real length we really want -logFile.extraColumns.Speed.length = 12; - -% open the file -logFile = saveEventsFile('open', cfg, logFile); -``` - - -## Functions descriptions - -### userInputs - -Get subject, run and session number and make sure they are positive integer values. - -By default this will return `cfg.subject.session = 1` even if you asked it to omit enquiring about sessions. This means -that the folder tree will always include a session folder. - -```matlab -[cfg] = userInputs(cfg) -``` - -if you use it with `cfg.subject.askGrpSess = [0 0]` -it won't ask you about group or session - -if you use it with `cfg.subject.askGrpSess = [1]` -it will only ask you about group - -if you use it with `cfg.subject.askGrpSess = [0 1]` -it will only ask you about session - -if you use it with `cfg.subject.askGrpSess = [1 1]` -it will ask you about both -this is the default - - -### createFilename - -Create the BIDS compliant directories and filenames (but not the files) for the behavioral -output for this subject / session / run. - -The folder tree will always include a session folder. - -Will also create the right filename for the eye-tracking data file. - -For the moment the date of acquisition is appended to the filename -- can work for behavioral experiment if cfg.testingDevice is set to 'PC' -- can work for fMRI experiment if cfg.testingDevice is set to 'mri' -- can work for simple eyetracking data if cfg.eyeTracker is set to 1 - -### saveEventsFile - -Function to save output files for events that will be BIDS compliant. - -If the user DOES NOT provide `onset`, `trial_type`, this events will be skipped. `duration` will be set to "NaN" if -no value is provided. - -### checkCFG - -Check that we have all the fields that we need in the experiment parameters. - -## CFG content - -```matlab -%% Can be modified by users -% but their effect might only be effective after running -% checkCFG - -cfg.verbose = 0; - -cfg.subject.subjectGrp = ''; -cfg.subject.sessionNb = 1; -cfg.subject.askGrpSess = [true true]; - -% BOLD MRI details -% some of those will be transferred to the correct fields in cfg.bids by checkCFG -cfg.mri.repetitionTime = []; -cfg.mri.contrastEnhancement = []; -cfg.mri.phaseEncodingDirection = []; -cfg.mri.reconstruction = []; -cfg.mri.echo = []; -cfg.mri.acquisition = []; - -cfg.fileName.task = ''; -cfg.fileName.zeroPadding = 3; % amount of 0 padding the subject, session, run number - -cfg.eyeTracker.do = false; - -% content of the json side-car file for bold data -cfg.bids.mri.RepetitionTime = []; -cfg.bids.mri.SliceTiming = ''; -cfg.bids.mri.TaskName = ''; -cfg.bids.mri.Instructions = ''; -cfg.bids.mri.TaskDescription = ''; - -% content of the json side-car file for MEG -cfg.bids.meg.TaskName = ''; -cfg.bids.meg.SamplingFrequency = []; -cfg.bids.meg.PowerLineFrequency = []; -cfg.bids.meg.DewarPosition = []; -cfg.bids.meg.SoftwareFilters = []; -cfg.bids.meg.DigitizedLandmarks = []; -cfg.bids.meg.DigitizedHeadPoints = []; - -% content of the datasetDescription.json file -cfg.bids.datasetDescription.Name = ''; -cfg.bids.datasetDescription.BIDSVersion = ''; -cfg.bids.datasetDescription.License = ''; -cfg.bids.datasetDescription.Authors = {''}; -cfg.bids.datasetDescription.Acknowledgements = ''; -cfg.bids.datasetDescription.HowToAcknowledge = ''; -cfg.bids.datasetDescription.Funding = {''}; -cfg.bids.datasetDescription.ReferencesAndLinks = {''}; -cfg.bids.datasetDescription.DatasetDOI = ''; - - -%% Should not be modified by users -% many of those fields are set up by checkCFG and you might get non BIDS valid -% output if you touch those -cfg.fileName.dateFormat = 'yyyymmddHHMM'; % actual date of the experiment that is appended to the filename -cfg.fileName.modality -cgf.fileName.suffix.mri -cgf.fileName.suffix.meg -cfg.fileName.stim -cfg.fileName.events -cfg.fileName.datasetDescription - -``` - -### createBoldJson - -``` -createBoldJson(cfg) -``` - -This function creates a very light-weight version of the side-car JSON file for a BOLD functional run. - -This will only contain the minimum BIDS requirement and will likely be less complete than the info you could from DICOM conversion. - -If you put the following line at the end of your experiment script, it will dump the content of the `extraInfo` structure in the json file. - -``` -createBoldJson(cfg, extraInfo) -``` - -This allows to add all the parameters that you used to run your experiment in a human readable format: so that when you write your methods sections 2 years later ("the reviewer asked me for the size of my fixation cross... FML"), the info you used WHEN you ran the experiment is saved in an easily accessible text format. For the love of the flying spaghetti monster do not save all your parameters in a `.mat` file: think of the case when you won't have matlab or octave installed on a computer (plus not everyone uses those). - -Also to reading your experiment parameters, you won't have to read it from the `setParameters.m` file and wonder if those might have been modified when running the experiment and you did not commit and tagged that change with git. - -## How to install - -### Download with git - -``` bash -cd fullpath_to_directory_where_to_install -# use git to download the code -git clone https://github.com/cpp-lln-lab/CPP_BIDS.git -# move into the folder you have just created -cd CPP_BIDS -# add the src folder to the matlab path and save the path -matlab -nojvm -nosplash -r "addpath(fullfile(pwd, 'src')); savepath ();" -``` - -Then get the latest commit: -```bash -# from the directory where you downloaded the code -git pull origin master -``` - -To work with a specific version, create a branch at a specific version tag number -```bash -# creating and checking out a branch that will be called version1 at the version tag v0.0.1 -git checkout -b version1 v0.0.1 -``` - -### Add as a submodule - -Add it as a submodule in the repo you are working on. - -``` bash -cd fullpath_to_directory_where_to_install -# use git to download the code -git submodule add https://github.com/cpp-lln-lab/CPP_BIDS.git -# move into the folder you have just created -cd CPP_BIDS -# add the src folder to the matlab path and save the path -matlab -nojvm -nosplash -r "addpath(fullfile(pwd, 'src'))" -``` - -To get the latest commit you then need to update the submodule with the information -on its remote repository and then merge those locally. -```bash -git submodule update --remote --merge -``` - -Remember that updates to submodules need to be committed as well. - -#### Example for submodule usage - -So say you want to clone a repo that has some nested submodules, then you would type this to get the content of all the submodules at once (here with assumption that you want to clone my experiment repo): -```bash -# clone the repo -git clone https://github.com/user_name/myExperiment.git - -# go into the directory -cd myExperiment - -# initialize and get the content of the first level of submodules (e.g. CPP_PTB and CPP_BIDS) -git submodule init -git submodule update - -# get the nested submodules JSONio and BIDS-matlab for CPP_BIDS -git submodule foreach --recursive 'git submodule init' -git submodule foreach --recursive 'git submodule update' -``` - -**TO DO** - - -### Direct download - -Download the code. Unzip. And add to the matlab path. - -Pick a specific version: - -https://github.com/cpp-lln-lab/CPP_BIDS/releases - -Or take the latest commit (NOT RECOMMENDED): - -https://github.com/cpp-lln-lab/CPP_BIDS/archive/master.zip - -**TO DO** - - -## Contributing +## 3. Contributing Feel free to open issues to report a bug and ask for improvements. -### Guidestyle +### 3.1. Guidestyle - We use camelCase. - We keep the McCabe complexity as reported by the [check_my_code function](https://github.com/Remi-Gau/check_my_code) below 15. - We use the [MISS_HIT linter](https://florianschanda.github.io/miss_hit/style_checker.html) to automatically fix some linting issues. -### BIDS naming convention +### 3.2. BIDS naming convention Here are the naming templates used. @@ -441,7 +92,7 @@ Here are the naming templates used. `sub-[_ses-