Commit a595f6a5 authored by Lauri Himanen's avatar Lauri Himanen
Browse files

Made a separate file and class for the optimade translation functionality.

parent e35871eb
Pipeline #93732 skipped with stage
This source diff could not be displayed because it is too large. You can view the blob instead.
let util = require('../common/util.js');
class Formula{
constructor(formula) {
this.formula = formula;
this.formulaMap = this._parseFormula(formula);
console.log('this.formulaMap: ', this.formulaMap);
}
_parseFormula(formula){
let index = 0;
let map = new Map();
let key;
while ( index < formula.length ){
let el2 = formula.substring(index, index+2);
let el1 = formula.substring(index, index+1);
if (util.ELEMENTS.indexOf(el2) >= 0){
map.set(el2, 1); // 1 default value
index += 2;
key = el2;
//console.log('eleemnt 2chars', key);
}else if (util.ELEMENTS.indexOf(el1) >= 0){
map.set(el1, 1); // 1 default value
index++;
key = el1;
//console.log('eleemnt 1chars', key);
}else{ // It's a number
let num = parseInt(el2);
if (num >= 10) index += 2; // 2 figures number
else index++;// 1 figure number
//console.log('number ', num, key);
map.set(key, num);
}
// console.log('FINAL LOOP', map, index);
}
return map;
}
getOptimadeSubquery(allowOtherElements) {
const fragments = []
this.formulaMap.forEach( (number, element)=> {
const fragment = '"' + element + (number === 1 ? '' : +number) + '"';
fragments.push(fragment)
})
return 'formula HAS ' + (allowOtherElements ? 'ALL ' : 'ONLY ') + fragments.join(', ')
}
getFragments() {
const fragments = []
this.formulaMap.forEach( (number, element)=> {
const fragment = '"' + element + (number === 1 ? '' : +number) + '"';
fragments.push(fragment);
})
return fragments;
}
/*
getReducedFormula(getTokens = true){
let counter = 0;
while ( !checkIfReduced(this.formulaMap) ){ // console.log('Reducing', this.formulaMap);
let div = 1;
if (isDivisibleBy(this.formulaMap, 2)) div = 2;
else if (isDivisibleBy(this.formulaMap, 3)) div = 3;
else if (isDivisibleBy(this.formulaMap, 5)) div = 5;
else if (isDivisibleBy(this.formulaMap, 7)) div = 7;
else if (isDivisibleBy(this.formulaMap, 11)) div = 11;
this.formulaMap.forEach( (value, key) => {
this.formulaMap.set(key, (value/div));
});
//console.log('Reducing DIV', this.formulaMap);
counter++;
if (counter > 5) break;
}
function checkIfReduced(formulaMap){
let min = 100;
formulaMap.forEach( (value, key) => {
if (value < min) min = value;
});
return min === 1;
}
function isDivisibleBy(formulaMap, n){
let div = true;
formulaMap.forEach( (value, key) => {
if (value % n !== 0) div = false;
});
return div;
}
let tokens = [];
let canonicalFormula = '';
if (getTokens){
this.formulaMap.forEach( (value, key) => tokens.push(key+value) );
}else{
let sortedElements = this._sortElements( Array.from( this.formulaMap.keys() ) );
sortedElements.forEach( element => {
canonicalFormula += element+this.formulaMap.get(element);
//canonicalFormula += element+(map.get(element) === 1 ? '' : map.get(element));
});
}
console.log('_reduceFormula RETURN: ', this.formulaMap, tokens, canonicalFormula);
return (getTokens ? tokens : canonicalFormula);
}
*/
}
// EXPORTS
module.exports = Formula;
......@@ -35,7 +35,7 @@ let FilterPanel = require('./FilterPanel.view.js');
let SwitchComponent = require('../common/SwitchComponent.js');
const REACTIVE_SEARCH = true;
const REACTIVE_SEARCH = false;
function replaceDashes(s){
return s.split('-').join('_');
......
let Formula = require('./Formula.js');
/**
* This class is used to translate the GUI query language into a corresponding
* Optimade query.
*/
class OptimadeTranslator {
constructor() {
}
/**
* Translates the given GUI query into the corresponding optimade query
* string.
*/
translate(queryIn, typesIn, allowOtherElements) {
// Make a copy so that this function becomes pure.
let query = [...queryIn];
let types = [...typesIn];
// Add escaped quotes to all elements
for (let i=0; i < query.length; ++i) {
let q = query[i];
let t = types[i];
if (t === "E") {
query[i] = `\"${q}\"`
}
}
// Add escaped quotes to formulas
for (let i=0; i < query.length; ++i) {
let q = query[i];
let t = types[i];
if (t === "F") {
const formula = new Formula(q)
const fragments = []
formula.formulaMap.forEach( (number, element)=> {
const fragment = '"' + element + (number === 1 ? '' : +number) + '"';
fragments.push(fragment)
})
query[i] = fragments.join(', ')
}
}
// Construct the reduced expression
[query, types] = this.simplify(query, types);
// Convert into optimade syntax
for (let i=0; i < query.length; ++i) {
let q = query[i];
let t = types[i];
if (t === "E") {
if (allowOtherElements) {
q = "elements HAS ALL " + q
} else {
q = "elements HAS ONLY " + q
}
} else if (t === "F") {
if (allowOtherElements) {
q = "formula HAS ALL " + q
} else {
q = "formula HAS ONLY " + q
}
}
query[i] = q;
}
let res = query.join(" ")
console.log("FINAL OPTIMADE QUERY:")
console.log(res)
return res
}
/**
* For combining elements/formulas connected by AND into a single list.
*/
combineANDIn(query, types) {
const newQuery = []
const newTypes = []
for (let i=0; i < query.length;) {
const q0 = query[i-1];
const t0 = types[i-1];
const q1 = query[i];
const t1 = types[i];
const q2 = query[i+1];
const t2 = types[i+1];
const q3 = query[i+2];
const t3 = types[i+2];
if (((t1 === "E" && t3 === "E") || (t1 === "F" && t3 === "F")) && q2 === "AND" && q0 !== "NOT") {
let active = true
let type = t3
let joined = [q1, q3]
i = i + 3
while(active) {
const q4 = query[i];
const t4 = types[i];
const q5 = query[i+1];
const t5 = types[i+1];
if (q4 === "AND" && t5 === type) {
joined.push(q5)
i += 2
} else {
active = false
newQuery.push(joined.join(", "))
newTypes.push(type)
}
}
} else {
newQuery.push(q1)
newTypes.push(t1)
i += 1
}
}
return [newQuery, newTypes]
}
/**
* For finding parenthesis pairs
*/
findPairs(query, types) {
const pairs = new Map();
// Find list of indices where parenthesis should be removed
let removed = []
for (let i=0; i < query.length; ++i) {
const q1 = query[i];
const t1 = types[i];
if (q1 === "(") {
let index = 0;
for (let j=i+1; j < query.length; ++j) {
const q2 = query[j];
const t2 = types[j];
if (q2 === "(") {
index += 1;
}
if (q2 === ")") {
if (index != 0) {
index -= 1;
} else {
pairs.set(i, j)
pairs.set(j, i)
break
}
}
}
}
}
return pairs;
}
/**
* For removing unnecessary parenthesis
*/
removeParenthesis(query, types) {
// Find list of indices where parenthesis should be removed
const pairs = this.findPairs(query, types)
let removed = []
for (let i=0; i < query.length; ++i) {
const q1 = query[i];
const t1 = types[i];
if (q1 === "(") {
let p = pairs.get(i)
// Remove outer parentheses
if (i === 0 && p == query.length-1) {
removed.push(p)
removed.push(i)
// Remove parentheses that do not contain multiple operations
} else {
const contents = query.slice(i+1, p)
const tp = types.slice(i+1, p)
let noop = true
for (let t of tp) {
if (t === "S") {
noop = false
break
}
}
if (noop) {
removed.push(p)
removed.push(i)
}
}
}
}
// Find repeated quotes and reduce them to single one
for (let i=0; i < query.length; ++i) {
const q1 = query[i];
const t1 = types[i];
const q2 = query[i+1];
const t2 = types[i+1];
if (q1 === "(" && q1 === "(") {
let p1 = pairs.get(i)
let p2 = pairs.get(i+1)
if (p2 == p1 - 1) {
removed.push(i)
removed.push(p1)
}
}
}
// Remove found parentheses
removed = removed.sort((a, b) => b - a);
for (let j=0; j < removed.length; ++j) {
query.splice(removed[j], 1);
types.splice(removed[j], 1);
}
return [query, types]
}
/**
* Combines AND statements where either one or both of the statements has a
* parenthesis. These statements are refactored using distributive laws of
* set theory:
* A AND (B OR C) == (A AND B) OR (A AND C)
* in order to see if there are lists of elements that should be joined.
*/
combineANDOut(query, types) {
let newQuery = query
let newTypes = types
const pairs = this.findPairs(query, types)
let combined = false
for (let i=0; i < query.length; ++i) {
// Look for AND surrounded by one or more parenthesis
const q1 = query[i];
const t1 = types[i];
const q2 = query[i+1];
const t2 = types[i+1];
const q3 = query[i+2];
const t3 = types[i+2];
if (q2 === "AND" && (q1 === ")" || q3 === "(" )) {
combined = true
// Get left hand and right hand side expressions
let lhsQ
let lhsT
let rhsQ
let rhsT
let rhsEnd
let lhsStart
if (q1 === ")") {
lhsStart = pairs.get(i)
lhsQ = query.slice(lhsStart, i+1)
lhsT = types.slice(lhsStart, i+1)
} else {
if (query[i-1] === "NOT") {
lhsStart = i-1
} else {
lhsStart = i
}
lhsQ = query.slice(lhsStart, i+1)
lhsT = types.slice(lhsStart, i+1)
lhsQ.unshift("(")
lhsT.unshift("P")
lhsQ.push(")")
lhsT.push("P")
}
if (q3 === "(") {
rhsEnd = pairs.get(i+2)+1
rhsQ = query.slice(i+2, rhsEnd)
rhsT = types.slice(i+2, rhsEnd)
} else {
if (query[i+2] === "NOT") {
rhsEnd = i+4
} else {
rhsEnd = i+3
}
rhsQ = query.slice(i+2, rhsEnd)
rhsT = types.slice(i+2, rhsEnd)
rhsQ.unshift("(")
rhsT.unshift("P")
rhsQ.push(")")
rhsT.push("P")
}
// Split the rhs into parts separated by OR
const rhsParts = []
let prevPos = 1
for (let j=1; j < rhsQ.length - 1; ++j) {
const sq1 = rhsQ[j];
if (sq1 === "OR") {
rhsParts.push([rhsQ.slice(prevPos, j), rhsT.slice(prevPos, j)])
prevPos = j+1
} else if (j === rhsQ.length - 2) {
rhsParts.push([rhsQ.slice(prevPos, j+1), rhsT.slice(prevPos, j+1)])
}
}
// Combine lhs with the different parts of rhs
let comboQ = []
let comboT = []
for (let j=0; j < rhsParts.length; ++j) {
let icomboQ = []
let icomboT = []
icomboQ = icomboQ.concat(rhsParts[j][0])
icomboT = icomboT.concat(rhsParts[j][1])
icomboQ.push("AND")
icomboT.push("S")
icomboQ = icomboQ.concat(lhsQ)
icomboT = icomboT.concat(lhsT)
if (j === 0) {
if (rhsParts.length > 1) {
comboQ.push("(")
comboT.push("P")
}
comboQ = comboQ.concat(icomboQ)
comboT = comboT.concat(icomboT)
if (rhsParts.length > 1) {
comboQ.push(")")
comboT.push("P")
}
} else {
comboQ.push("OR")
comboT.push("S")
comboQ.push("(")
comboT.push("P")
comboQ = comboQ.concat(icomboQ)
comboT = comboT.concat(icomboT)
comboQ.push(")")
comboT.push("P")
}
}
comboQ.unshift("(")
comboT.unshift("P")
comboQ.push(")")
comboT.push("P")
newQuery = query.slice(0, lhsStart).concat(comboQ).concat(query.slice(rhsEnd, query.length))
newTypes = types.slice(0, lhsStart).concat(comboT).concat(types.slice(rhsEnd, query.length))
break
}
}
return [newQuery, newTypes, combined]
}
/**
* Recursive function for simplifying the query into a form that can be
* correctly interpreted by Optimade.
*/
simplify(query, types) {
// Remove unnecessary parenthesis, combine all elements connected with
// AND, and remove unnecessary parenthesis again.
[query, types] = this.removeParenthesis(query, types);
[query, types] = this.combineANDIn(query, types);
[query, types] = this.removeParenthesis(query, types);
// Combine parenthesis statements
let combined
[query, types, combined] = this.combineANDOut(query, types);
// If a combination was done, run recursively until no combination is
// done
if (combined) {
[query, types] = this.simplify(query, types)
// After the last step perform on final cleanup
} else {
[query, types] = this.removeParenthesis(query, types);
[query, types] = this.combineANDIn(query, types);
[query, types] = this.removeParenthesis(query, types);
}
return [query, types]
}
}
// EXPORTS
module.exports = OptimadeTranslator;
......@@ -24,6 +24,8 @@
"use strict";
let util = require('../common/util.js');
let OptimadeTranslator = require('./OptimadeTranslator.js');
let Formula = require('./Formula.js');
//
//
//// local utility functions
......@@ -82,344 +84,19 @@ class SearchBox{
}
getOptimadeQuery2(allowOtherElements) {
let query = [...this.searchQuery];
let types = [...this.queryTypes];
// For combining elements/formulas connected by AND into a single list
function combineANDIn(query, types) {
const newQuery = []
const newTypes = []
for (let i=0; i < query.length;) {
const q0 = query[i-1];
const t0 = types[i-1];
const q1 = query[i];
const t1 = types[i];
const q2 = query[i+1];
const t2 = types[i+1];
const q3 = query[i+2];
const t3 = types[i+2];
if (((t1 === "E" && t3 === "E") || (t1 === "F" && t3 === "F")) && q2 === "AND" && q0 !== "NOT") {
let active = true
let type = t3
let joined = [q1, q3]
i = i + 3
while(active) {
const q4 = query[i];
const t4 = types[i];
const q5 = query[i+1];
const t5 = types[i+1];
if (q4 === "AND" && t5 === type) {
joined.push(q5)
i += 2
} else {
active = false
newQuery.push(joined.join(", "))
newTypes.push(type)
}
}
} else {
newQuery.push(q1)
newTypes.push(t1)
i += 1
}
}
return [newQuery, newTypes]
}
// For finding parenthesis pairs
function findPairs(query, types) {
const pairs = new Map();
// Find list of indices where parenthesis should be removed
let removed = []
for (let i=0; i < query.length; ++i) {
const q1 = query[i];
const t1 = types[i];
if (q1 === "(") {
let index = 0;
for (let j=i+1; j < query.length; ++j) {
const q2 = query[j];
const t2 = types[j];
if (q2 === "(") {
index += 1;
}
if (q2 === ")") {
if (index != 0) {
index -= 1;
} else {
pairs.set(i, j)
pairs.set(j, i)
break
}
}
}
}
}
return pairs;
}
// For removing unnecessary parenthesis
function removeParenthesis(query, types) {