const VALID_ELEMENTS = 'H He Li Be B C N O F Ne Na Mg Al Si P S Cl Ar Kr K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Ar Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe Cs Ba La-Lu Hf Ta W Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn Fr Ra Ac-Lr Rf Db Sg Bh Hs Mt Ds Rg Cn La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr'.split( ' ' ); // from pymatgen.core.periodic_table import get_el_sp // from pymatgen import Element // element_symbols = [e.symbol for e in Element] // elements__X_asc = sorted(element_symbols, // key=lambda s: (get_el_sp(s).X, s)) const ELEMENTS_X_ASC = 'Ar Eu He Lr Ne Pm Rn Tb Yb Fr Cs K Rb Ba Ra Na Sr Li Ca Ac La Ce Pr Nd Sm Gd Dy Y Ho Er Tm Lu Pu Am Bk Cf Cm Es Fm Hf Md No Th Mg Zr Np Sc U Pa Ta Ti Mn Be Nb Al Tl V Zn Cr Cd In Ga Fe Co Cu Re Si Tc Ni Ag Sn Hg Po Ge Bi B Sb Te Mo As P At H Ir Os Pd Ru Pt Rh Pb W Au C Se S Xe I Br Kr N Cl O F'.split( ' ' ); export const MatgenUtilities = { orderByElectronegativity: function (elements: any[], data) { if (data == undefined) data = ELEMENTS_X_ASC; return elements; /*return _.sortBy(elements, function(e) { return data.indexOf(e); }); not sure it would have work*/ }, isNumber: function (tok) { if (tok == null) return false; if (tok.search(/\d+\.?\d*/) >= 0) return true; else return false; }, isCloseParens: function (tok) { if (tok == ')') return true; else return false; }, isElement: function (elm, data = VALID_ELEMENTS) { if (data.indexOf(elm) !== -1) { return true; } else { return false; } }, // Parse a string of element states. // eg. // 'Fe2+ Fe3+ O2-' => { Fe: [2, 3], O: [-2]} // parseElNumList: function (states) { // > states='Fe2+ Fe3+ O2-' // > states.match(/[A-Z][a-z]?\d+[+-]?/g) // [ 'Fe2+', 'Fe3+', 'O2-' ] const tokens = states.match(/[A-Z][a-z]?\d+[+-]?/g); const state_hash = {}; const tok_len = tokens.length; for (let i = 0; i < tok_len; i++) { // > "Fe2+".match(/([A-Z][a-z]?)(\d+)([+-]?)/) // [ 'Fe2+', 'Fe', '2', '+', index: 0, input: 'Fe2+' ] const components = tokens[i].match(/([A-Z][a-z]?)(\d+)([+-]?)/); const el = components[1]; const num = parseInt('' + components[3] + components[2], 10); if (!Array.isArray(state_hash[el])) { state_hash[el] = []; } state_hash[el].push(num); } return state_hash; }, // Explicitly add 1s to array insertOnes: function (tokens) { let i; let new_toks: any = []; for (i = 0; i < tokens.length; i++) { if ( (MatgenUtilities.isElement(tokens[i]) || MatgenUtilities.isCloseParens(tokens[i])) && !MatgenUtilities.isNumber(tokens[i + 1]) ) { new_toks.push(tokens[i]); new_toks.push('1'); } else { new_toks.push(tokens[i]); } } return new_toks; }, // Return Formula tokens: elements, numbers, (, ) tokenize: function (formula) { return formula.match(/([A-Z][a-z]?|\d+\.?\d*|[()])/g); }, parseUnitcellFormula: function (formulaString: string) { let formula; formula = formulaString.replace(/[*]/g, '').trim(); formula = formulaString.replace(/-/g, '').trim(); // ?????? if (true || MatgenUtilities.isValidWildcard(formulaString)) { return []; } else { if (formula.match(/[^\d\w\s().]/)) { throw "Illegal character in formula: '" + formula + "'"; } let test_formula = formula.match(/(\(?[A-Z][a-z]?\d*\.?\d*\)?\d*\.?\d*)/g); if (test_formula != null) { test_formula = test_formula.join(''); } // Make sure things were in the right format. Break down the formula and put it back together if (test_formula != formula && !MatgenUtilities.isValidWildcard(formulaString)) { throw "Invalid formula: '" + formula + "'"; } // Tokens are elements, numbers, and parens const tokens = MatgenUtilities.tokenize(formula); // Explicitly add 1s to simplify parsing of implied value 1 // eg. H2O -> ['H', '2', 'O'] -> ['H', '2', 'O', '1'] const expanded_tokens = MatgenUtilities.insertOnes(tokens); let ans: any = {}; for (let i = 0; i < expanded_tokens.length; i = i + 2) { ans[expanded_tokens[i]] = parseInt(expanded_tokens[i + 1]); } return ans; } }, isValidWildcard: function (string) { let elements = string.split(/-/g); if (elements.indexOf('*') > -1) { return elements.every(function (el) { return MatgenUtilities.isElement(el) || el == '*'; }); } else { const m = string.match(/([^A-Z]|^)+[a-z]|[^\.\w(),]+/g); const a = Array.from(new Set(m)); //return _.isEqual(_.uniq(m), ["*"]); return a[0] === '*'; } }, /** * Parses a chemical formula and returned a normalized sanitized formula. * * @param {string} formula A chemical formula. * * @return {hash} A hash containing [ elements, sanitized_formula ]. For * example, Li4Fe4P4O16 will return [ ['Li', 'Fe', 'O'], 'LiFePO4'] */ parseFormula: function (formula) { const cleanformula = formula.replace(/\s/g, ''); let m = cleanformula.match(/([^A-Z]|^)+[a-z]|[^\.\w(),]+/g); if (m != null && !MatgenUtilities.isValidWildcard(cleanformula)) { return null; } const re = /([A-Z][a-z]*)([\d\.]*)/; m = cleanformula.match(/([A-Z][a-z]*)([\d\.]*)/g); let elements: string[] = []; let amounts = {}; let i; console.log('checking formula...'); if (m != null) { console.log('still checking...'); for (i = 0; i < m.length; i++) { const m2 = re.exec(m[i]); if (VALID_ELEMENTS.indexOf(m2![1]) === -1) { return null; } if (elements.indexOf(m2![1]) === -1) { elements.push(m2![1]); if (m2![2] == '') { amounts[m2![1]] = 1; } else { amounts[m2![1]] = m2![2]; } } else { if (m2![2] == '') { amounts[m2![1]] += 1; } else { amounts[m2![1]] += m2![2]; } } } const amtnum: any[] = []; for (const el in amounts) { amtnum.push(amounts[el]); } const gcd = MatgenUtilities.getGCD(amtnum); const form_santized: any[] = []; for (i = 0; i < elements.length; i++) { form_santized.push(elements[i] + amounts[elements[i]] / gcd); } console.log('valid formula'); return [elements.sort().join('&'), form_santized.sort().join(' ')]; } return null; }, /** * Returns the greatest common denominator of two numbers. * * @param {integer} a First number. * @param {integer} b Second number. * * @return {integer} Greatest common denominator. */ gcd: function (a, b) { let w, x, y; x = a; y = b; while (y != 0) { w = x % y; x = y; y = w; } return x; }, /** * Returns the greatest common denominator of a sequence of numbers. * * @param {array} numbers An array of numbers. * * @return {integer} Greatest common denominator. */ getGCD: function (numbers) { let GCD = numbers[0]; for (let i = 1; i < numbers.length; i++) { GCD = MatgenUtilities.gcd(GCD, numbers[i]); } return GCD; }, /** * Returns a HTML version of a formula, with proper subscripts. */ htmlFormula: function (formula): string { let htmlFormula = formula.replace('PCO7', 'PO4CO3'); const oxidNumRegex = /([A-Z][a-z]*)([\d\.]+(?=-|\+).)/g; const oxidRegex = /([A-Z][a-z]*)(-|\+)/g; if (!htmlFormula.match(oxidRegex) && !htmlFormula.match(oxidNumRegex)) { htmlFormula = htmlFormula.replace(/([A-Z][a-z]*)([\d\.]+)/g, '$1$2'); htmlFormula = htmlFormula.replace(/(\))([\d\.]+)/g, '$1$2'); } else { htmlFormula = htmlFormula.replace(oxidNumRegex, '$1$2'); htmlFormula = htmlFormula.replace(oxidRegex, '$1$2'); } return '' + htmlFormula + ''; }, validateInput: function (text, type) { let re; if (type == 'text') { re = /^[A-Za-z0-9\s\&\|\-\(\)]*$/; } else if (type == 'numeric') { re = /^[0-9\.\-\s\&\|<>]*$/; } else if (type == 'boolean') { re = /^(t|f)$/; } return re.test(text); }, cleanDecimals: function (num, decimalPlaces, fixed) { const numStr = new Number(num).toFixed(decimalPlaces); if (fixed) { return numStr; } const clean = parseFloat(numStr); if (isNaN(clean)) { return ''; } else { return clean; } }, stripSpace: function (text) { return text.replace(/^\s+|\s+$/g, ''); }, };