function outPoly = commstr2poly(txt, varargin)
%COMMSTR2POLY Obtain a polynomial representation from a textual description.
%   P = COMMSTR2POLY(STR) returns coefficients of a polynomial described by
%   string STR as a row vector of coefficients P, in order of ascending
%   powers.
%
%   COMMSTR2POLY(STR, OUTFMT) specifies the output polynomial format as
%   either 'ascending', 'descending', 'decimal', 'octal' or 'galois'.
%   Partial strings can be provided, as long as they can uniquely map to
%   one of the possible options. When OUTFMT is 'ascending' or
%   'descending', the output is a numeric vector that contains the
%   polynomial coefficients in the specified power order. When OUTFMT is
%   'decimal', the output is a scalar decimal integer, whose binary
%   representation denotes the polynomial in order of descending powers.
%   When OUTFMT is 'octal', the output is a scalar octal number, whose
%   binary representation denotes the polynomial in order of descending
%   powers. For the 'decimal' and 'octal' output formats, STR must
%   represent a binary-coefficient polynomial. 'When OUTFMT is 'galois',
%   the output is a Galois field array. If omitted, OUTFMT defaults to
%   'ascending'.
%
%   COMMSTR2POLY(STR, 'galois', PRIMPOLY) returns a Galois field array.
%   PRIMPOLY is the primitive polynomial of the Galois field array. If
%   omitted, it defaults to D^4 + D + 1. PRIMPOLY can be a decimal scalar,
%   a polynomial string or a binary vector.
%
%   COMMSTR2POLY(STR, 'OUTFMT', PRIMPOLY, PARSEALPHA) can parse
%   coefficients that are powers of the primitive element alpha, when
%   PARSEALPHA is true. If omitted, PARSEALPHA defaults to false.
%
%   C = COMMSTR2POLY(CSTR, ...) returns separate polynomial coefficients
%   for each polynomial string in CSTR which is either a cell array of
%   character vectors or a string vector. C is a cell array of numeric
%   vectors that has the same number of elements as CSTR.
%
%   -----------------------------------------------------------------------
%   DETAILED OPERATION
%
%   Power signs: commstr2poly can also handle negative powers (use case for
%   scramblers). The powers of the polynomial variable need to be either
%   all positive or all negative.
%
%   The input string should describe a polynomial using a mathematical
%   syntax that includes the plus symbol (+), a non-negative integer
%   constant, and polynomial terms expressing a non-negative coefficient
%   with a variable name raised to a power. The caret (^) symbol may appear
%   between the variable name and the power. The asterisk symbol (*) may
%   appear between the coefficient and the variable. Spaces are ignored,
%   and ordering of terms is unimportant. No other mathematical operators
%   may be present. For example, the following strings describe the same
%   polynomial:
%
%   STR = '1+2*x+x^3+4x^5+x^14'
%   STR = 'q^14 + 4q^5 + q^3 + 2q + 1'
%   STR = '1+2m+m3+4*m5+m14'
%   STR = 'x14 + 4x5 + x3 + 2x + 1'
%
%   Text appearing before the polynomial expression will be ignored if the 
%   text is a function declaration such as "g(x)" where parentheses are 
%   present, with or without an equals sign following it. For example, the 
%   following describe the same polynomial as above:
%
%   STR = 'g(x) = 1+2x+x3+4x5+x14'
%   STR = 'g(m) 1+2m+m3+4m5+m14'
%
%   Powers may be individually surrounded by braces, as often occurs when 
%   copying text from a web page. For example, the following describe the 
%   same polynomial as above:
%
%   'p(x) = x^{14} + 4x^5 + x^3 + 2x^1 + 1'
%   'p(x) = x{14}+4x{5}+x{3}+2x{1}+1'
%
%   Empty strings and strings starting with a hash tag (#), exclamation
%   mark (!), percent sign (%) or semicolon (;) in column 1 are ignored and
%   an empty coefficient vector is returned. The polynomial terms may
%   appear within a set of square brackets, and some trailing punctuation
%   (periods, commas, semicolons and colons) is permitted and ignored, as
%   in this example:
%
%       commstr2poly('[D14 + 4D5 + D3 + 2D + 1], ...')
%       ans =
%            1  2  0  1  0  4  0  0  0  0  0  0  0  0  1
%
%   This function also accepts unicode superscripts:
%       http://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts
%   and full-width characters (jp/cn/kr):
%       http://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms
%
%   -----------------------------------------------------------------------
%
%   EXAMPLES
%       gfpretty(COMMSTR2POLY('g(x) = 1+2x+x3+4x5+x14.'))
%                         3      5    14
%              1 + 2 X + X  + 4 X  + X
%
%       COMMSTR2POLY('[1 + 2*q + q^3 + 4Q5 + q^8]', 'descending')
%       ans =
%
%         1     0     0     4     0     1     0     2     1
%
%       COMMSTR2POLY('D^4+D^3+1', 'decimal')
%       ans =
%
%           25
%
%       COMMSTR2POLY('D^4+D^3+1', 'octal')
%       ans =
% 
%           31
%
%       COMMSTR2POLY('x4 + 3*x^3 + x^2 + 2X + 3', 'galois', 'D^3+D+1')
%       ans = 
%           GF(2^4) array. Primitive polynomial = D^3+D+1 (11 decimal)
%           Array elements =
%              1           3           1           2           3
%
%       parseAlpha = true;
%       gfpretty(commstr2poly('x4 + a^13x^3 + a^6*x2 + a3x + a10', ...
%                                      'ascending', 'D^4+D^1+1', parseAlpha))
%                           2       3    4
%             7 + 8 X + 12 X  + 13 X  + X 
%       
%       gfpretty(commstr2poly('x4 + alpha^13x^3 + alpha^6*x2 + alpha3x + alpha10', ...
%                                      'ascending', 'D^4+D^1+1', parseAlpha))
%                           2       3    4
%             7 + 8 X + 12 X  + 13 X  + X 
%
%       COMMSTR2POLY('1+z^-6+z-8')
%       ans =
%
%         1     0     0     0     0     0     1     0     1
%
%   See also GFCONV, GFPRETTY, GF, OCT2POLY, HEX2POLY, POLY2TRELLIS.

%   Copyright 2015-2021 The MathWorks, Inc.
    
    % default values:
    fmt         = 'ascending';
    primpoly    = 19; % D^4+D^1+1
    M           = 4;
    parseAlpha  = false;

    if nargin >= 2
            
        % commstr2poly(str, 'outfmt')
        fmt = validatestring(varargin{1}, {'ascending', 'descending', 'decimal', 'octal', 'galois'});
        
        if nargin >= 3            
            % commstr2poly(str, 'outfmt', primpoly)

            % parse possible string representation:
            ppIn = varargin{2};
            if comm.internal.utilities.isCharOrStringScalar(ppIn)
                primpoly = commstr2poly(ppIn, 'decimal');
            elseif length(ppIn) > 1 % binary vector in descending powers
                if any(ppIn > 1) || any(ppIn < 0)
                    id = 'comm:commstr2poly:InvalidPrimitivePolynomial';
                    error(id, getString( message(id) ) );
                end
                primpoly = bit2int(ppIn', length(ppIn)); % make order ascending, required by gftuple
            else % decimal scalar
                primpoly = ppIn;
            end
            validateattributes(primpoly, {'numeric'}, {'scalar', 'integer', 'real', '>', 0}, '' ,'primitive polynomial');
            M = ceil(log2(max(1,double(primpoly)))-1);

            if nargin == 4
                % commstr2poly(STR, 'OUTFMT', PRIMPOLY, PARSEALPHA)
                validateattributes(varargin{3}, {'logical'}, {'scalar'}, '' ,'parseAlpha');
                parseAlpha = varargin{3};
            end            
        end
    end

    % Determine # of strings, force into a cell-vector
    inHasMultiplePolys = false;
    if comm.internal.utilities.isCharOrStringScalar(txt)
        txt = {txt};
    elseif iscellstr(txt)
        inHasMultiplePolys = true;
    elseif (isstring(txt) && ~isscalar(txt))
        inHasMultiplePolys = true;
        txt = cellstr(txt);
    else
        id = 'comm:commstr2poly:MustBeString';
        error(id, getString( message(id) ) );
    end

    Ns          = numel(txt);   % # strings
    allPolys    = cell(Ns,1);   % the output vector for the cell-input case

    for j = 1:Ns                % for each input string
        
        % convert superscript and full-width characters to equivalent ascii chars:        
        txt{j} = convertToEquivalentAsciiString(txt{j});
        
        % split input string on +, remove "redundant" characters and
        % initialize helper struct polyParams:
        polyParams = init(txt{j}, parseAlpha);
        
        Nt = numel(polyParams.terms);  
        for i = 1:Nt            % for each term in this string
            
            % find and remove the variable name, e.g., 
            % 3x^5 -> power: 5 with termCoeff:3
            [polyParams, varPresent, termCoeff] = findVarName(polyParams, i);
            
            % store coefficients in helper variable polyParams:
            if varPresent   % e.g., 3x^5
                polyParams = parseTerm(polyParams, i, termCoeff);
            else            % e.g., 3
                polyParams = parseConstant(polyParams, i);
            end
        end

        % get all coeffs, including 0s at the highest order, as they may
        % represent alpha^0
        outPoly = polyParams.coeffs;
        
        if polyParams.primElemFound
            % outPoly contains powers of alpha, convert those to equivalent
            % binary elements:            
            gfTemp      = gftuple(outPoly', M);
            outPoly     = bit2int(gfTemp', M, false); % lsb-first
        end

        % Optional descending order:
        if strcmpi(fmt, 'descending')
            % invert the order:
            outPoly = fliplr(outPoly);

        elseif strcmpi(fmt, 'decimal')

            if any(outPoly>1) || any(outPoly<0)
                id = 'comm:commstr2poly:FormatNeedsBinary';
                error(id, getString( message(id, 'decimal') ) );
            end
            
            if ~isempty(outPoly)
                % convert binary polynomial to integer scalar:
                outPoly = bit2int(outPoly', length(outPoly), false); 
            else
                outPoly = 0;
            end

        elseif strcmpi(fmt, 'octal')

            if any(outPoly>1) || any(outPoly<0)
                id = 'comm:commstr2poly:FormatNeedsBinary';
                error(id, getString( message(id, 'octal') ) );
            end

            if ~isempty(outPoly)
                % convert binary vector to equivalent octal scalar:
                outPoly = str2double(dec2base(bit2int(outPoly', length(outPoly), false), 8)); 
            else
                outPoly = 0;
            end
            
        elseif strcmpi(fmt, 'galois')
            
            outPoly = gf(fliplr(outPoly), M, primpoly);
            
        % else outPoly is already in ascending order
        end

        % cache result for cell-input, cell-output case:
        allPolys{j} = outPoly;
    end

    % For the cell-input & cell-output case, return the cell array
    % containing all cached results:
    if inHasMultiplePolys
        outPoly = allPolys;
    end
end

function polyParams = init(txt, parseAlpha)

    % Setup for term iterations:
    polyParams.txt                   = txt;
    polyParams.terms                 = cleanupFirstNLastTerms(splitIntoTerms(txt));
    polyParams.powers                = [];
    polyParams.coeffs                = [];
    polyParams.Vname                 = '';      % variable name
    polyParams.variableFound         = false;
    polyParams.negativePowers        = false;
    polyParams.positivePowers        = false;
    polyParams.constSpecified        = false;
    polyParams.parseAlpha            = parseAlpha;
    
    % if polyParams.parseAlpha is true then polyParams.coeffs can have
    % alpha powers, i.e., not necessarily scalar left-side coefficients
    polyParams.primElemFound         = false;
end


function terms = splitIntoTerms(txt)
% Return cell-vector of individual terms.
% Split only on + symbols, leaving symbols in substrings.
%
% Substitute an empty string for a "comment" string.
% Comments are strings that being with: '#','%','!',';'

    terms = {};

    % do not process comment lines
    % Do NOT deblank first!  Comment char MUST be first char.
    commentChars = {'#','%','!',';'};
    if (~isempty(txt) && any(strcmpi(txt(1),commentChars))) || ...
       (length(txt)>1 && strcmpi(txt(1:2), '\\')) % lines beginning with '\\' are also comments
        % comment line returns empty terms
        return
    end

    % do not process essentially empty strings
    txt = strtrim(txt); % deblank
    if isempty(txt)
        return
    end

    % remove first character if it is a '+'
    if txt(1) == '+'
        if length(txt) == 1
            return
        else
            txt = txt(2:end);
        end
    end
    
    % split string on '+' delimiters:
    res     = textscan(txt,'%s', 'Delimiter', '+');
    terms   = res{1}; % textscan returns a 1x1 cell array containing a cell array of strings
end

function [polyParams, isVarPresent, termCoeff] = findVarName(polyParams, idx)
    % Return isVarPresent = true if term is not constant
    %
    % termCoeff: string representation of coefficient preceding the varName
    % of this variable, e.g., '2' from '2x'. Variable name might not be
    % present, in which case termCoeff=''. Variable name might be present
    % but no preceding coefficient, in which case termCoeff will equal '1'.
    termCoeff = '';

    % Don't remove spaces from term. The function should accept "2 x^2", 
    % but reject "foo bar". If space is collapsed, "foo bar" becomes 
    % "foobar", and it looks like a simple variable "foobar".
    term = polyParams.terms{idx};

    % Search for a new variable name (Vname).
    % Example terms: 0, 1, x0, x, x1, x^5, x(5), x{5}, 3*x5, 2x^7,
    % 5lpha^2, alpha13*x3
    
    greekLowerChar      = '\x{03B1}-\x{03C9}';
    greekUpperChar      = '\x{0391}-\x{03A9}';
    greekOrEnglishChar  = ['[a-z_A-Z_' greekLowerChar '_' greekUpperChar ']'];
    decimalNumber       = '[0-9]*\.?[0-9]*';
    earlySign           = '[-+]?[\s]*';
    spacesOrMultStar    = '\s*[*]?\s*';
    primitiveElement    = [greekOrEnglishChar '*\s*\^?\s*[0-9]*'];
    
    if polyParams.parseAlpha
        primitiveElementConsideration = primitiveElement;
    else
        primitiveElementConsideration = '';
    end
    expression = ['^(?<coeff>' earlySign decimalNumber primitiveElementConsideration ')' spacesOrMultStar '(?<vname>' greekOrEnglishChar '+)'];
    [tokens,~,Nchar]    = regexp(term, expression, 'names');
    
    isVarPresent = ~isempty(tokens);
    if isVarPresent % else function returns

        termCoeff = strtrim(tokens.coeff);
        
        if ~polyParams.variableFound   % this may happen only under idx == 1 or 2
            % We had no explicit Vname until now, so capture it:
            polyParams.Vname = tokens.vname;
            polyParams.variableFound = true; % now we have an explicit var name
        
        else  % this is at least the 2nd time that we encounter a variable 
                        
            if ~strcmpi(polyParams.Vname, tokens.vname) % we now parse a term that has a new variable name

                if polyParams.parseAlpha % only allowable if one of the two encounters was a^i (zero-power of the variable, e.g., x)
                    
                    if (strcmp(tokens.vname, 'x') || strcmp(tokens.vname, 'X')) && idx == 2
                        % first element was constant, power of primitive elment
                        % (with no var name)
                        % polyParams.coeffs must now be [0 0 0 1], where the length is
                        % the power of the exponent
                        temp = length(polyParams.coeffs)-1;
                        polyParams.coeffs = [];
                        polyParams.coeffs(1) = temp;
                        polyParams.Vname = tokens.vname;
                    elseif strcmp(polyParams.Vname, 'x') || strcmp(polyParams.Vname, 'X')
                        % current element is a power of the primitive element with
                        % no var_name, e.g., a3
                        termCoeff = term(Nchar+1:end); % make a^N the coefficient
                        termCoeff(termCoeff == '^') = '';
                        polyParams.powers{idx} = '0'; % multiply with x^0
                        return
                    end
                else
                    id = 'comm:commstr2poly:MultiplePolyVars';
                    error(id, getString( message(id, tokens.vname, term, polyParams.Vname, polyParams.txt) ) );
                end
            end
        end                
        
        if polyParams.parseAlpha
            
            % look for a primitive element, specifically the index of its last character
            primElemPos = max(regexp(termCoeff, greekOrEnglishChar));
            
            % if the coefficient has a Greek or English character (e.g., a or alpha), 
            % it is considered to be expressed with a power of the primitive element
            
            if ~isempty(primElemPos) 
                if ~polyParams.primElemFound 
                    % If this is the first time the primitive element is found,
                    % previous "1" coefficients must be converted to 0 (a^0 = 1)
                    % as coefficients will now be expressing powers of alpha.
                    % Also, previous 0 coefficients (absent powers) should be
                    % converted to -inf
                    polyParams.coeffs(polyParams.coeffs == 0) = -Inf;
                    polyParams.coeffs(polyParams.coeffs == 1) = 0;                
                end
                % switch parsing to primitive-element mode:
                polyParams.primElemFound = true;

                % keep the exponent only:
                termCoeff = strtrim(termCoeff( primElemPos+1:end ));
                if isempty(termCoeff)   % no exponent, coeff = alpha
                    termCoeff = '1';    % save the exponent
                else
                    termCoeff(termCoeff == '^') = '';   % remove possible power sign
                    exponent = sscanf(termCoeff,'%g');  % save the exponent
                    if isempty(exponent)
                        id = 'comm:commstr2poly:InvalidPrimitivePower';
                        error(id, getString( message(id, termCoeff, polyParams.txt) ) );
                    end 
                end
                
            elseif polyParams.primElemFound  % A primitive element had been identified earlier
                termCoeff='0';  % in this mode exponents are saved. alpha^0 = 1
            end
            
        else % no primitive elements in this term, coeff is either N * x or N or X
            
            if isempty(termCoeff) % Absent coefficient is equivalent to 1                                
                termCoeff='1';
            end
        end

        % Remove everything before possible trailing "power" string in the 
        % term string. parseTerm only handles powers.
        % Examples:'2x^3'->'^3', '2x'->'', 'x5'->'5'
        polyParams.powers{idx} = strtrim(term(Nchar+1:end));
    end
end

function polyParams = parseTerm(polyParams, idx, termCoeff)
% Polynomial variable present
%
% Variable name vname is no longer present in t_i.
% Examples:
%   'x', 'x2', 'x^3', 'x{4}', 'x(5)'
% which are now just
%   '', '2', '^3', '{4}', '(5)'
% since vname has been removed from the terms when we get here.
%
% Such terms are not supported:
%   'x 2', 'x foo'
% Here they look like:
%  '2', 'foo'
%
% Any leading coefficient is captured in termCoeff string.

    % Spaces are NOT removed here, so that 'x foo bar' is rejected and the
    % error message shows 'foo bar' as the power term, not 'foobar'.
    
    powerStr = polyParams.powers{idx};

    if isempty(powerStr)
        % just x ... no x1 or x^1, but it means the same thing.
        powerNum = 1;
    else
        % Good: '2', '^2' (numeric preceded by optional caret)
        % Bad: 'foo' (non-numeric)
        orig_power = powerStr;
        powerNum = [];

        if powerStr(1) == '^' || powerStr(1) == '.'
            powerStr = strtrim(powerStr(2:end));
        end
        
        if ~isempty(powerStr)
            
            % allow powers specified with braces, e.g., X{P}
            openBrace = any(powerStr(1) == '{');
            if openBrace
                powerStr = strtrim(powerStr(2:end));
            end

            powerStr(powerStr == ' ') = ''; % remove spaces, e.g., - 2
            if ~isempty(powerStr)
                
                hyphenMinusSign = '-'; % the one that is typed in MATLAB
                longMinusSign = 8722; % longer minus sign, one found in HTML pages
                % capture negative power:
                if powerStr(1) == hyphenMinusSign || powerStr(1) == longMinusSign 
                    multFactor = -1;
                    powerStr(1) = ''; % remove minus sign
                else
                    multFactor = 1;
                end
                    
                [powerNum,~,~,nxt] = sscanf(powerStr,'%g');
                if numel(powerNum)~=1 || ... % empty or more than 1 element
                        nxt < numel(powerStr)  % or if more chars remain
                    if contains(orig_power, '-')
                        id = 'comm:commstr2poly:InvalidNegativePower';
                        error(id, getString( message(id) ) );
                    else
                        id = 'comm:commstr2poly:InvalidPower';
                        error(id, getString( message(id, orig_power, polyParams.txt) ) );
                    end
                end
                
                powerNum = multFactor * powerNum; % restore sign
                
                % fractional powers are not allowed
                if powerNum ~= fix(powerNum)
                    id = 'comm:commstr2poly:NonIntegerPower';
                    error(id, getString( message(id, num2str(powerNum), polyParams.txt) ) );
                end
                powerStr = strtrim(powerStr(nxt:end));
            end

            if openBrace
                % throw warning if a brace was opened but not closed
                if isempty(powerStr) || ~any(powerStr(1)=='}')
                    id = 'comm:commstr2poly:MissingCloseBrace';
                    warning(id, getString( message(id, orig_power, polyParams.txt) ) );
                end
                powerStr = powerStr(2:end);
            end

            % If anything left in powerStr, there's a problem
            if ~isempty(powerStr)
                id = 'comm:commstr2poly:InvalidPower';
                error(id, getString( message(id, orig_power, polyParams.txt) ) );
            end
        end
    end

    dupconst = (powerNum == 0) && polyParams.constSpecified;  % Repeated specification of const

    % power can be either all positive or all negative
    if ( powerNum > 0 && polyParams.negativePowers == true ) || ... 
            ( powerNum < 0 && polyParams.positivePowers == true )
        id = 'comm:commstr2poly:MixedPowerSigns';
        error(id, getString( message(id, powerNum, polyParams.txt) ) );
    end
    
    if powerNum > 0 && polyParams.positivePowers == false
        % first-time only, enter positive-powers mode
        polyParams.positivePowers = true;
    end
    
    if powerNum < 0
        if polyParams.negativePowers == false
            % first-time only, enter negative-powers mode
            polyParams.negativePowers = true;
        end
        powerNum = - powerNum; % store z^-2 at the same location with z^2
    end
    
    % Check if this power is already specified.
    pidx = powerNum+1; % entry into vector
    if dupconst || ( numel(polyParams.coeffs) >= pidx && ...
            (polyParams.primElemFound == 0 && polyParams.coeffs(pidx) ~= 0  || ...
             polyParams.primElemFound == 1 && polyParams.coeffs(pidx) ~= -inf) )
        id = 'comm:commstr2poly:DuplicatePower';
        error(id, getString( message(id, powerNum, polyParams.txt) ) );
    end
    
    polyParams.constSpecified = polyParams.constSpecified || powerNum==0;

    % Get leading coefficient of this term.
    if isempty(termCoeff)
        coeff = 1;
    else
        % Remove all internal spaces, so '+ 1' looks like '+1'.
        termCoeff(termCoeff == ' ') = '';
        coeff = sscanf(termCoeff,'%g');
        if isempty(coeff)
            id = 'comm:commstr2poly:InvalidCoefficient';
            error(id, getString( message(id, termCoeff, polyParams.txt) ) );
        end
    end

    % if d.primElemFound == true, then resizing the vector should not fill
    % the "empty" entries with zeros, because a^0 = true. It should fill
    % these with -Inf    
    if polyParams.primElemFound
        prevLen = length(polyParams.coeffs);
        for i = max(1, prevLen + 1):pidx-1
            polyParams.coeffs(i) = -Inf; % otherwise polyParams.coeffs(pidx) would expand with 0s
        end
    end
    
    polyParams.coeffs(pidx) = coeff;
end

function polyParams = parseConstant(polyParams,idx)
% Parse constant term.
% Assume no polynomial variable present.

    % remove both outer and internal whitespaces:
    term = strtrim(polyParams.terms{idx});
    term(term==' ') = '';
    
    if strcmp(term, '0')
        % do nothing for addition of 0
        return
    end

    if polyParams.primElemFound && term == '1'
        % Some standards use primitive elements for power
        % coefficients, but 1 (instead of a^0) for the constant, e.g.:
        % cdma2000: x5 + a111x4 + a160x3 + a160x2+ a111x + 1.
        % Make the constant compliant with the general alpha-mode:
        term = '0'; 
    end
    
    % get constant value:
    [const,~,~,nxt] = sscanf(term,'%g');
    if nxt < numel(term) || isempty(const)
        id = 'comm:commstr2poly:InvalidConstant';
        error(id, getString( message(id, term, polyParams.txt) ) );
    end

    dupc = polyParams.constSpecified; % Duplicate specification of const

    const_idx = 1; % 0'th order term
    % if constant has already been encountered:
    if dupc || ( numel(polyParams.coeffs) >= const_idx && ...
         (  (~polyParams.primElemFound && polyParams.coeffs(const_idx) ~= 0) || ...
            ( polyParams.primElemFound && polyParams.coeffs(const_idx) ~= -inf)   ) )
        id = 'comm:commstr2poly:DuplicateConstant';
        error(id, getString( message(id, term, polyParams.txt) ) );
    end
    
    polyParams.constSpecified = true;
    polyParams.coeffs(const_idx) = const; % order-0 element is index 1
end

function terms = cleanupFirstNLastTerms(terms)
% Remove stray text outside the polynomial.
%
% First term may include leading "g(x)" or "m = " that must be removed.
%
% Brackets may surround polynomial terms, but cannot appear outside a
% leading "g(x) =".
%
% Trailing commas and periods are removed.

    if isempty(terms)
        return % EARLY EXIT
    end

    % Remove leading/trailing space from each term in cell-vector:
    terms       = strtrim(terms);
    firstTerm   = terms{1};
    lastTerm    = terms{end}; % could be same as firstTerm

    % Last term:
    %  - trailing period, comma, semicolon
    %  - identify/remove spaces intermixed with those punctuation marks
    %  - can be multiple in succession, like ' ...' or '; ...'
    %
    % Technique: find first non-punctuation/space char, starting from end.
    %
    % Must include a space in this punctuation list, otherwise embedded spaces
    % between punctuation will abort the punctuation cleanup at that char.
    trailingPunct = '.,;: ';
    loa = ismember(lastTerm, trailingPunct);
    idx = find(~loa, 1, 'last');
    if ~isempty(idx)
        % Truncate sequence of punctuation, adjusting for detecting 1st
        % non-punct in flipped string:
        lastTerm = lastTerm(1:idx);
    end

    % First term:
    % - remove everything at/before equals sign
    % - Example: "g(x) = 1 + x2 + ... "
    idx = find(firstTerm == '=');
    if ~isempty(idx)
        firstTerm = strtrim(firstTerm(idx+1:end));
    end

    % First term:
    % - remove everything at/before closed-paren
    % - Example: "g(x) 1 + x2 + ..."
    idx = find(firstTerm == ')');
    if ~isempty(idx)
        firstTerm = strtrim(firstTerm(idx+1:end));
    end

    % First and last term:
    % - remove encapsulating square brackets or parentheses from first/last
    %   terms, if they match
    % - Example: [1+ X^3 +X^4]
    if ~isempty(firstTerm) && ~isempty(lastTerm)
        % Must be the SAME style of matching char at beginning/end of
        % polynomial, otherwise something went wrong
        sel1 = firstTerm(1) == '[(';
        sel2 = lastTerm(end) == '])';
        if any(sel1 & sel2)     % use one ampersand, not two!
            firstTerm = strtrim(firstTerm(2:end));
            lastTerm = strtrim(lastTerm(1:end-1));
        end
    end

    % Return any changes:
    terms{1}    = firstTerm;
    terms{end}  = lastTerm; % could be same as firstTerm

end

function plainStr = convertToEquivalentAsciiString(str)

    plainStr = str;
    
    % Replace unicode superscripts with ascii digits
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    superscriptChars0To9 = char(hex2dec(['2070'; '00B9'; '00B2'; '00B3'; '2074'; '2075'; '2076'; '2077'; '2078'; '2079']));
    for i = 0:9
        plainStr(plainStr == superscriptChars0To9(i+1)) = num2str(i);
    end
    
    
    % Convert full-width characters, which can be specified with jp/cn/kr keyboards
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    
    % Convert full-width numbers:
    for i = 0:9 % '\x{FF10}-\x{FF19}'
        fullWidthCharI =  char(hex2dec(['FF1' num2str(i)]));
        plainStr(plainStr == fullWidthCharI) = num2str(i);
    end
    
    % Convert full-width uppercase Latin letters
    fullWidthUpperCaseA = 'FF21';
    asciiUpperCaseA     = 65;
    for i = 1:26 % A-Z
        thisChar = char(hex2dec(fullWidthUpperCaseA) + i-1); % A-Z have continuous codes
        plainStr(plainStr == thisChar) = char(asciiUpperCaseA + i-1);
    end
        
    % Convert full-width lowercase Latin letters
    fullWidthLowerCaseA = 'FF41';
    asciiLowerCaseA     = 97;
    for i = 1:26 % a-z
        thisCode = char(hex2dec(fullWidthLowerCaseA) + i-1); % a-z have continuous codes
        plainStr(plainStr == thisCode) = char(asciiLowerCaseA + i-1);
    end
    
    % Convert full-width symbols:
    symbolToFullWidthCodeMap = {    '!',    'FF01'; ...
                                    '#',    'FF03'; ...
                                    '%',    'FF05'; ...
                                    '(',    'FF08'; ...
                                    ')',    'FF09'; ...
                                    '*',    'FF0A'; ...
                                    '+',    'FF0B'; ...
                                    '-',    'FF0D'; ...
                                    ';',    'FF1B'; ...
                                    '=',    'FF1D'; ...
                                    '[',    'FF3B'; ...
                                    '\',    'FF3C'; ...
                                    ']',    'FF3D'; ...
                                    '^',    'FF3E'; ...
                                    '{',    'FF5B'; ...
                                    '}',    'FF5D'};
                                
    for i = 1:length(symbolToFullWidthCodeMap)
        indexes             = (plainStr == char(hex2dec(symbolToFullWidthCodeMap{i, 2})));
        plainStr(indexes)   = symbolToFullWidthCodeMap{i, 1};
    end
end

% [EOF]
