Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions code/+nansen/+config/+dloc/+dsmconversion/Report.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
classdef (Sealed) Report < handle
%Report Collects the model elements a conversion did not map, and why
%
% A handle, so that the helpers of a conversion can add to one report
% without passing it back.

properties (SetAccess = private)
Element (:,1) string = string.empty(0, 1)
Reason (:,1) string = string.empty(0, 1)
end

methods
function add(obj, element, reason, condition)
%add Record an element and the reason it was not mapped
%
% report.add(element, reason, condition) records only when
% condition is true, so that callers can report conditionally
% without wrapping each call in an if block.

arguments
obj
element (1,1) string
reason (1,1) string
condition (1,1) logical = true
end

if ~condition
return
end
obj.Element(end+1, 1) = element;
obj.Reason(end+1, 1) = reason;
end

function reportTable = toTable(obj)
%toTable The report as a table with Element and Reason columns
reportTable = table(obj.Element, obj.Reason, ...
'VariableNames', {'Element', 'Reason'});
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
function [fileNameExpression, fileType, isConverted] = convertFilePatternToWildcard(dsmPattern, sessionTokens)
%convertFilePatternToWildcard Convert a file pattern regex to a NANSEN file name expression
%
% Syntax:
% [fileNameExpression, fileType, isConverted] = ...
% convertFilePatternToWildcard(dsmPattern, sessionTokens)
%
% A Dataset Structure Model file pattern is a regular expression that may
% contain {token} references to an entity's metadata. NANSEN's
% FileNameExpression is a wildcard pattern anchored with ^ and $, applied
% in the session folder. NANSEN already restricts a virtual session
% folder to the files that contain the session id, so tokens naming the
% session's identity become *:
%
% ^{recording_id}\.ABF$ -> ^*.ABF$ with file type .ABF
%
% Escaped punctuation becomes literal and .* becomes *. Any other regex
% construct, or a token that does not name the session identity, has no
% wildcard equivalent; isConverted is then false.
%
% Input arguments:
% dsmPattern - The file pattern from the Dataset Structure Model.
% sessionTokens - Names of the metadata fields that identify a session.

arguments
dsmPattern (1,1) string
sessionTokens (1,:) string
end

fileNameExpression = "";
fileType = "";
isConverted = false;

extension = regexp(dsmPattern, "\\\.([A-Za-z0-9]+)\$$", "tokens", "once");
if ~isempty(extension)
fileType = "." + extension{1};
end

tokens = regexp(dsmPattern, "\{([A-Za-z_][A-Za-z0-9_]*)\}", "tokens");
pattern = dsmPattern;
for i = 1:numel(tokens)
if ~ismember(tokens{i}{1}, sessionTokens)
return
end
pattern = replace(pattern, "{" + tokens{i}{1} + "}", "*");
end

pattern = char(pattern);
wildcard = '';
i = 1;
while i <= numel(pattern)
c = pattern(i);
if c == '\'
if i == numel(pattern) || isstrprop(pattern(i+1), 'alphanum')
return % \d, \w and the like have no wildcard equivalent
end
wildcard(end+1) = pattern(i+1); %#ok<AGROW>
i = i + 2;
elseif c == '.' && i < numel(pattern) && pattern(i+1) == '*'
wildcard(end+1) = '*'; %#ok<AGROW>
i = i + 2;
elseif c == '*' || (c == '^' && i == 1) || (c == '$' && i == numel(pattern)) ...
|| ~any(c == '.[](){}+?|^$')
wildcard(end+1) = c; %#ok<AGROW>
i = i + 1;
else
return
end
end

fileNameExpression = string(wildcard);
isConverted = true;
end
119 changes: 119 additions & 0 deletions code/+nansen/+config/+dloc/+dsmconversion/convertRegexToWholeMatch.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
function [nansenPattern, isConverted] = convertRegexToWholeMatch(dsmPattern)
%convertRegexToWholeMatch Rewrite a capture group regex so its whole match is the value
%
% Syntax:
% [nansenPattern, isConverted] = convertRegexToWholeMatch(dsmPattern)
%
% A Dataset Structure Model extracts the first capture group of a regular
% expression. NANSEN's expr mode returns the whole match instead. The
% text before and after the first capture group is moved into lookbehind
% and lookahead assertions, so that the whole match is the group:
%
% ^(\d{6}_\d+)[a-z]\.ABF$ -> (?<=^)\d{6}_\d+(?=[a-z]\.ABF$)
%
% A pattern without a capture group is returned unchanged. A pattern
% whose group is quantified, or whose surrounding text has a top level
% alternation, can not be rewritten this way; isConverted is then false
% and nansenPattern is empty.

arguments
dsmPattern (1,1) string
end

nansenPattern = "";
isConverted = false;

[groupStart, groupEnd] = findFirstCaptureGroup(char(dsmPattern));

if isempty(groupStart)
nansenPattern = dsmPattern;
isConverted = true;
return
end

pattern = char(dsmPattern);
prefix = pattern(1:groupStart-1);
group = pattern(groupStart+1:groupEnd-1);
suffix = pattern(groupEnd+1:end);

isQuantified = ~isempty(suffix) && any(suffix(1) == '*+?{');
if isQuantified || hasTopLevelAlternation(prefix) || hasTopLevelAlternation(suffix)
return
end

if hasTopLevelAlternation(group)
group = ['(?:', group, ')'];
end

nansenPattern = string(group);
if ~isempty(prefix)
nansenPattern = "(?<=" + prefix + ")" + nansenPattern;
end
if ~isempty(suffix)
nansenPattern = nansenPattern + "(?=" + suffix + ")";
end
isConverted = true;
end

function [groupStart, groupEnd] = findFirstCaptureGroup(pattern)
%findFirstCaptureGroup Indices of the parentheses of the first capturing group
groupStart = [];
groupEnd = [];
depth = 0;
inClass = false;
i = 1;
while i <= numel(pattern)
c = pattern(i);
if c == '\'
i = i + 2;
continue
elseif inClass
inClass = c ~= ']';
elseif c == '['
inClass = true;
elseif c == '('
isCapturing = ~(i < numel(pattern) && pattern(i+1) == '?');
if isempty(groupStart) && isCapturing
groupStart = i;
depth = 1;
elseif ~isempty(groupStart)
depth = depth + 1;
end
elseif c == ')' && ~isempty(groupStart)
depth = depth - 1;
if depth == 0
groupEnd = i;
return
end
end
i = i + 1;
end
groupStart = [];
end

function tf = hasTopLevelAlternation(pattern)
%hasTopLevelAlternation True when | appears outside groups and character classes
tf = false;
depth = 0;
inClass = false;
i = 1;
while i <= numel(pattern)
c = pattern(i);
if c == '\'
i = i + 2;
continue
elseif inClass
inClass = c ~= ']';
elseif c == '['
inClass = true;
elseif c == '('
depth = depth + 1;
elseif c == ')'
depth = depth - 1;
elseif c == '|' && depth == 0
tf = true;
return
end
i = i + 1;
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
function [mode, input, isConverted] = convertSliceToExtraction(slice)
%convertSliceToExtraction Convert a 0-based slice to a NANSEN ind or expr rule
%
% Syntax:
% [mode, input, isConverted] = convertSliceToExtraction(slice)
%
% A Dataset Structure Model substring rule is a Python style slice,
% "start:stop", 0-based and half-open, where either bound may be omitted
% and a negative bound counts from the end. NANSEN's ind mode takes a
% 1-based MATLAB index expression.
%
% Slices whose bounds are both known positions become ind rules:
% 0:6 -> ind 1:6
% : -> ind 1:end
% Slices that depend on the length of the text become expr rules, so
% that they do not rely on end appearing anywhere but in 1:end:
% 9: -> expr (?<=^.{9}).*
% :-4 -> expr ^.*(?=.{4}$)
%
% A slice with a negative start and a non-negative stop depends on the
% length of the text in a way neither mode can express; isConverted is
% then false and mode and input are empty.

arguments
slice (1,1) string
end

mode = "";
input = "";
isConverted = false;

bounds = split(slice, ":");
if numel(bounds) ~= 2
return
end

start = parseBound(bounds(1));
stop = parseBound(bounds(2));

if isnan(start); start = 0; end

if start >= 0 && ~isnan(stop) && stop >= 0
mode = "ind";
input = sprintf("%d:%d", start + 1, stop);

elseif start == 0 && isnan(stop)
mode = "ind";
input = "1:end";

elseif start > 0 && isnan(stop)
mode = "expr";
input = sprintf("(?<=^.{%d}).*", start);

elseif start >= 0 && stop < 0
mode = "expr";
if start == 0
input = sprintf("^.*(?=.{%d}$)", -stop);
else
input = sprintf("(?<=^.{%d}).*(?=.{%d}$)", start, -stop);
end

elseif start < 0 && isnan(stop)
mode = "expr";
input = sprintf(".{%d}$", -start);

elseif start < 0 && stop < 0 && stop > start
mode = "expr";
input = sprintf(".{%d}(?=.{%d}$)", stop - start, -stop);

else
return
end

isConverted = true;
end

function value = parseBound(bound)
%parseBound An integer bound, or NaN when the bound is omitted
if strlength(strtrim(bound)) == 0
value = NaN;
else
value = str2double(bound);
end
end
Loading
Loading