From 822614820197d8b7a0710582e54724c0b5b858d2 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Mon, 17 Jan 2022 00:22:57 +0300 Subject: [PATCH 1/9] Suggest compounds Search through all available compounds, populate suggestions, sort the suggestions by formula length. All these are performed in the browser since all the compound formulas and elements are already fetched. Signed-off-by: Mehmet Baker --- components/RightSideBar.vue | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/components/RightSideBar.vue b/components/RightSideBar.vue index 4583c70..dc099fa 100644 --- a/components/RightSideBar.vue +++ b/components/RightSideBar.vue @@ -63,6 +63,11 @@ +
+
+ {{ compound.formula }} +
+
@@ -157,6 +162,7 @@ export default { foundCompound: { dtp_names: [], }, + suggestedCompounds: [], loadingInstance: null, timeoutCheck: null, } @@ -203,6 +209,25 @@ export default { this.$store.commit('SET_PROBABLE_ELEMENTS', []) }, methods: { + setElementsByFormula(formula) { + const formulaElements = Array.from(formula.matchAll(/[A-Z][a-z0-9]*/g)).map((match) => match[0]) + const elements = this.$store.getters.elements + const compoundElements = [] + + for (const formulaElement of formulaElements) { + const [, symbol, count] = formulaElement.match(/([A-Za-z]+)(\d*)/) + const element = elements.find((e) => e.symbol === symbol) + + if (element) { + compoundElements.push({ + count: Number(count || 1), + ...element, + }) + } + } + + this.elements = compoundElements + }, getAvailableElements(elements) { this.loadingInstance = Loading.service({ background: 'rgba(0, 0, 0, 0.7)', lock: true }) this.foundCompound = { @@ -220,6 +245,14 @@ export default { const compounds = this.$store.getters.compounds const availableCompoundElements = [] const finalized = [] + const suggestions = [] + const inputElementRegExps = elements.map((element) => { + if (element.count > 1) { + return new RegExp(`${element.symbol}${element.count}`) + } + + return new RegExp(`${element.symbol}(?![a-f0-9])`) + }) compounds.forEach((compound) => { let symbol = '' @@ -246,6 +279,12 @@ export default { pushAvailableElement(index) }) availableCompoundElements.push(availableElements) + + const forbidden = compound.formula.includes('?') + if (!forbidden && inputElementRegExps.every((regex) => regex.test(compound.formula))) { + suggestions.push(compound) + } + function pushAvailableElement(index) { if (chars.length - 1 === index) { availableElements.push({ @@ -256,6 +295,9 @@ export default { } }) + suggestions.sort((a, b) => a.formula.length - b.formula.length) + this.suggestedCompounds = suggestions.slice(0, 5) + let exactCompound = null availableCompoundElements.forEach((availableElements) => { let isValidCompound = true @@ -511,4 +553,22 @@ export default { } } } + +.suggestion-area { + color: #fff; + display: flex; + width: 20vw; + flex-wrap: wrap; + gap: 5px; + justify-content: center; + margin-top: 30px; + + .suggested-compound { + background: #10b710; + padding: 2px 10px; + border-radius: 8px; + user-select: none; + cursor: pointer; + } +} From 790ff5327a51f95d801a3d6316c6a336c2c167ce Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Mon, 17 Jan 2022 00:49:35 +0300 Subject: [PATCH 2/9] Improved compound suggestions - Don't expect exact element counts if there are less than 5 suggestions. - Don't suggest the selected compound in the suggestions list. Signed-off-by: Mehmet Baker --- components/RightSideBar.vue | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/components/RightSideBar.vue b/components/RightSideBar.vue index dc099fa..5d5aeb9 100644 --- a/components/RightSideBar.vue +++ b/components/RightSideBar.vue @@ -295,8 +295,30 @@ export default { } }) + if (suggestions.length < 5) { + suggestions.splice(0, 5) + const inputElementMatchStrings = elements.map((e) => `${e.symbol}${e.count === 1 ? '' : e.count}`) + + for (const compound of compounds) { + const forbidden = compound.formula.includes('?') + if (!forbidden && inputElementMatchStrings.every((str) => compound.formula.includes(str))) { + suggestions.push(compound) + } + } + } + suggestions.sort((a, b) => a.formula.length - b.formula.length) - this.suggestedCompounds = suggestions.slice(0, 5) + + this.suggestedCompounds = suggestions.slice(0, 5).filter((suggestion) => { + let { formula } = suggestion + + for (const element of elements) { + const re = new RegExp(`[A-Z0-9]?${element.symbol}${element.count === 1 ? '' : element.count}`) + formula = formula.replace(re, '') + } + + return formula.length + }) let exactCompound = null availableCompoundElements.forEach((availableElements) => { From 37035aca991fe602f8702a117de49399af95bd26 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Mon, 17 Jan 2022 23:10:35 +0300 Subject: [PATCH 3/9] Added `fcf` directive too format compound formulas with subscripts Signed-off-by: Mehmet Baker --- nuxt.config.js | 1 + plugins/directives.js | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 plugins/directives.js diff --git a/nuxt.config.js b/nuxt.config.js index 95910cb..190c948 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -24,6 +24,7 @@ module.exports = { { src: '~/plugins/element-ui', ssr: false }, { src: '~/plugins/vue-gtag.js', ssr: false, mode: 'client' }, { src: '~/plugins/vue-sanitize', ssr: false }, + { src: '~/plugins/directives', ssr: false }, ], // Auto import components: https://go.nuxtjs.dev/config-components diff --git a/plugins/directives.js b/plugins/directives.js new file mode 100644 index 0000000..8b2857d --- /dev/null +++ b/plugins/directives.js @@ -0,0 +1,7 @@ +import Vue from 'vue' + +Vue.directive('fcf', { + inserted(el) { + el.innerHTML = el.innerHTML.replace(/(\d+)/g, `$1`) + }, +}) From 4a38124e6587295a21631841f2bc01bf95511068 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Mon, 17 Jan 2022 23:14:13 +0300 Subject: [PATCH 4/9] Performance optimizations and cosmetics Resolves evrimagaci/periodum#5 Signed-off-by: Mehmet Baker --- components/RightSideBar.vue | 86 ++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/components/RightSideBar.vue b/components/RightSideBar.vue index 5d5aeb9..f661bac 100644 --- a/components/RightSideBar.vue +++ b/components/RightSideBar.vue @@ -64,7 +64,7 @@
-
+
{{ compound.formula }}
@@ -245,14 +245,52 @@ export default { const compounds = this.$store.getters.compounds const availableCompoundElements = [] const finalized = [] - const suggestions = [] - const inputElementRegExps = elements.map((element) => { + + // this holds the suggestions with exact count matches + const primaryCompoundSuggestions = new Map() + + // this holds a loose match; will match the atoms (symbols) only + const secondaryCompoundSuggestions = new Map() + + const primaryRegExps = elements.map((element) => { if (element.count > 1) { + // if there are more than 1 atoms, filter the compounds accordingly return new RegExp(`${element.symbol}${element.count}`) } + // if there is only one atom, perform a negative look-ahead search to filter out compounds with multiple counts return new RegExp(`${element.symbol}(?![a-f0-9])`) }) + const secondaryRegExps = elements.map((e) => new RegExp(`${e.symbol}${e.count === 1 ? '' : e.count}`)) + + const performMatch = (compound, regexps, map) => { + let compoundHasAllElements = true + + for (const regex of regexps) { + if (!regex.test(compound.formula)) { + compoundHasAllElements = false + break + } + } + + if (compoundHasAllElements) { + if (map.size < 5) { + map.set(compound.formula, compound) + } else { + let longestFormula = '' + for (const key of map.keys()) { + if (longestFormula.length < key.length) { + longestFormula = key + } + } + + if (compound.formula.length < longestFormula.length) { + map.delete(longestFormula) + map.set(compound.formula, compound) + } + } + } + } compounds.forEach((compound) => { let symbol = '' @@ -281,8 +319,10 @@ export default { availableCompoundElements.push(availableElements) const forbidden = compound.formula.includes('?') - if (!forbidden && inputElementRegExps.every((regex) => regex.test(compound.formula))) { - suggestions.push(compound) + + if (!forbidden) { + performMatch(compound, primaryRegExps, primaryCompoundSuggestions) + performMatch(compound, secondaryRegExps, secondaryCompoundSuggestions) } function pushAvailableElement(index) { @@ -295,21 +335,21 @@ export default { } }) - if (suggestions.length < 5) { - suggestions.splice(0, 5) - const inputElementMatchStrings = elements.map((e) => `${e.symbol}${e.count === 1 ? '' : e.count}`) + while (primaryCompoundSuggestions.size < 5) { + const [key, value] = secondaryCompoundSuggestions.entries().next().value + secondaryCompoundSuggestions.delete(key) + primaryCompoundSuggestions.set(key, value) - for (const compound of compounds) { - const forbidden = compound.formula.includes('?') - if (!forbidden && inputElementMatchStrings.every((str) => compound.formula.includes(str))) { - suggestions.push(compound) - } + if (secondaryCompoundSuggestions.size === 0) { + break } } - suggestions.sort((a, b) => a.formula.length - b.formula.length) + const suggestedCompounds = Array.from(primaryCompoundSuggestions.values()) + suggestedCompounds.sort((a, b) => a.formula.length - b.formula.length) - this.suggestedCompounds = suggestions.slice(0, 5).filter((suggestion) => { + // filter out the current compound formula from the suggestions (since it is already being shown) + this.suggestedCompounds = suggestedCompounds.filter((suggestion) => { let { formula } = suggestion for (const element of elements) { @@ -577,7 +617,6 @@ export default { } .suggestion-area { - color: #fff; display: flex; width: 20vw; flex-wrap: wrap; @@ -585,12 +624,19 @@ export default { justify-content: center; margin-top: 30px; - .suggested-compound { - background: #10b710; - padding: 2px 10px; - border-radius: 8px; + .compound-suggestion { + background: linear-gradient(136deg, #272f3f 0%, #1d232f 100%); + padding: 7px 22px; + border-radius: 4px; user-select: none; cursor: pointer; + color: #fff; + opacity: 0.7; + transition: opacity 0.3s ease; + + &:hover { + opacity: 1; + } } } From 8b1b5af964330ff9a54b2550166d92cceb2cc193 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Wed, 19 Jan 2022 20:41:14 +0300 Subject: [PATCH 5/9] Refactored `setElementsByFormula` Signed-off-by: Mehmet Baker --- components/RightSideBar.vue | 44 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/components/RightSideBar.vue b/components/RightSideBar.vue index f661bac..a83a90f 100644 --- a/components/RightSideBar.vue +++ b/components/RightSideBar.vue @@ -209,24 +209,48 @@ export default { this.$store.commit('SET_PROBABLE_ELEMENTS', []) }, methods: { - setElementsByFormula(formula) { - const formulaElements = Array.from(formula.matchAll(/[A-Z][a-z0-9]*/g)).map((match) => match[0]) - const elements = this.$store.getters.elements - const compoundElements = [] + parseCompoundFormula(formula) { + /** + * Group 1: The upper case letter of the symbol (e.g. "N" for Na). All symbols have + * 1 and only 1 upper case letter; which is always the first character. + * Group 2: The remaining lower case letter(s) if there are any. + * Group 3: Element count (if specified) (e.g. "2" for O2) + */ + const regex = /([A-Z])([a-z]*)(\d*)/g + const compoundSymbols = [] + let match + + while ((match = regex.exec(formula)) !== null) { + const [, symbolUpperCaseChar, symbolLowerCaseChars, symbolCount] = match + compoundSymbols.push({ + symbol: `${symbolUpperCaseChar}${symbolLowerCaseChars}`, + count: symbolCount ? Number(symbolCount) : 1, + }) + } - for (const formulaElement of formulaElements) { - const [, symbol, count] = formulaElement.match(/([A-Za-z]+)(\d*)/) - const element = elements.find((e) => e.symbol === symbol) + return compoundSymbols + }, + getElementBySymbol(symbol) { + return this.$store.getters.elements.find((e) => e.symbol === symbol) + }, + setElementsByFormula(formula) { + const formulaSymbols = this.parseCompoundFormula(formula) + const elements = [] + for (const { symbol, count } of formulaSymbols) { + const element = this.getElementBySymbol(symbol) if (element) { - compoundElements.push({ - count: Number(count || 1), + elements.push({ + count, ...element, }) + } else { + console.error(`There is no element with the symbol "${symbol}".`) + return } } - this.elements = compoundElements + this.elements = elements }, getAvailableElements(elements) { this.loadingInstance = Loading.service({ background: 'rgba(0, 0, 0, 0.7)', lock: true }) From a2446b45e64ebb5909d1e8c2c6d58cd00df513c4 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Wed, 19 Jan 2022 23:25:18 +0300 Subject: [PATCH 6/9] Simplified `setElementsByFormula` Signed-off-by: Mehmet Baker --- components/RightSideBar.vue | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/components/RightSideBar.vue b/components/RightSideBar.vue index a83a90f..c2942d2 100644 --- a/components/RightSideBar.vue +++ b/components/RightSideBar.vue @@ -234,23 +234,12 @@ export default { return this.$store.getters.elements.find((e) => e.symbol === symbol) }, setElementsByFormula(formula) { - const formulaSymbols = this.parseCompoundFormula(formula) - const elements = [] - - for (const { symbol, count } of formulaSymbols) { - const element = this.getElementBySymbol(symbol) - if (element) { - elements.push({ - count, - ...element, - }) - } else { - console.error(`There is no element with the symbol "${symbol}".`) - return + this.elements = this.parseCompoundFormula(formula).map(({ symbol, count }) => { + return { + count, + ...this.getElementBySymbol(symbol), } - } - - this.elements = elements + }) }, getAvailableElements(elements) { this.loadingInstance = Loading.service({ background: 'rgba(0, 0, 0, 0.7)', lock: true }) From 71954c21c16ffb8fc173d6e5b65161f75aecd074 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Thu, 3 Feb 2022 00:21:28 +0300 Subject: [PATCH 7/9] Added store mock Signed-off-by: Mehmet Baker --- package.json | 1 + tests/__mocks__/store/index.js | 77 ++++++++++++++++++++++++++++++++++ yarn.lock | 2 +- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/__mocks__/store/index.js diff --git a/package.json b/package.json index 2d32cd4..1da6ad8 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@testing-library/jest-dom": "^5.16.1", "@testing-library/user-event": "^13.5.0", "@testing-library/vue": "^5.8.2", + "@vue/test-utils": "^1.3.0", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^10.1.0", "babel-jest": "^26", diff --git a/tests/__mocks__/store/index.js b/tests/__mocks__/store/index.js new file mode 100644 index 0000000..6a87819 --- /dev/null +++ b/tests/__mocks__/store/index.js @@ -0,0 +1,77 @@ +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) + +export const getters = { + elements: jest.fn().mockReturnValue([]), + temperature: jest.fn().mockReturnValue(100), + compounds: jest.fn().mockReturnValue([]), + isotopes: jest.fn().mockReturnValue([]), + probableElements: jest.fn().mockReturnValue([]), + isIsotopeFetched: jest.fn().mockReturnValue(true), + isCompoundFetched: jest.fn().mockReturnValue(true), + isMobile: jest.fn().mockReturnValue(false), + isOriented: jest.fn().mockReturnValue(false), +} + +export const mutations = { + UPDATE_TEMPERATURE: jest.fn(), + UPDATE_VIEW_TEMPERATURE: jest.fn(), + DEACTIVATE_TEMPERATURE: jest.fn(), + SET_TEMPERATURE_TYPE: jest.fn(), + SET_ELEMENTS: jest.fn(), + SET_COMPOUNDS: jest.fn(), + SET_ISOTOPES: jest.fn(), + SET_PROBABLE_ELEMENTS: jest.fn(), + SET_COMPOUNDS_FETCHED: jest.fn(), + SET_ISOTOPES_FETCHED: jest.fn(), + SHOW_INFO_MODAL: jest.fn(), + SET_SELECTED_CONTENT_ID: jest.fn(), + SET_SEARCH_TEXT: jest.fn(), + SET_IS_MOBILE: jest.fn(), + SET_IS_ORIENTED: jest.fn(), +} + +export const actions = {} + +export const state = { + app: {}, + elements: [], + compounds: [], + isotopes: [], + probableElements: [], + temperature: 0, + selectedTemperatureType: 'c', + isTemperatureTriggered: false, + isIsotopeFetched: false, + isCompoundFetched: false, + showInfoModal: false, + selectedContentId: null, + searchText: null, + isMobile: false, + isOriented: false, +} + +// eslint-disable-next-line no-underscore-dangle +export function __createMocks(custom = { getters: {}, mutations: {}, actions: {}, state: {} }) { + const mockGetters = Object.assign({}, getters, custom.getters) + const mockMutations = Object.assign({}, mutations, custom.mutations) + const mockActions = Object.assign({}, actions, custom.actions) + const mockState = Object.assign({}, state, custom.state) + + return { + getters: mockGetters, + mutations: mockMutations, + actions: mockActions, + state: mockState, + store: new Vuex.Store({ + getters: mockGetters, + mutations: mockMutations, + actions: mockActions, + state: mockState, + }), + } +} + +export const store = __createMocks().store diff --git a/yarn.lock b/yarn.lock index dd28e2e..1b7af5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2365,7 +2365,7 @@ optionalDependencies: prettier "^1.18.2 || ^2.0.0" -"@vue/test-utils@^1.1.0": +"@vue/test-utils@^1.1.0", "@vue/test-utils@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.3.0.tgz#d563decdcd9c68a7bca151d4179a2bfd6d5c3e15" integrity sha512-Xk2Xiyj2k5dFb8eYUKkcN9PzqZSppTlx7LaQWBbdA8tqh3jHr/KHX2/YLhNFc/xwDrgeLybqd+4ZCPJSGPIqeA== From e1ed71fc383f5980dee1a9443565bd62c346d251 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Thu, 3 Feb 2022 00:25:31 +0300 Subject: [PATCH 8/9] Added style mock Signed-off-by: Mehmet Baker --- jest.config.js | 1 + tests/__mocks__/styleMock.js | 1 + 2 files changed, 2 insertions(+) create mode 100644 tests/__mocks__/styleMock.js diff --git a/jest.config.js b/jest.config.js index 969a027..0d9ef87 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,6 @@ module.exports = { moduleNameMapper: { + '\\.(css|scss)$': '/tests/__mocks__/styleMock.js', '^@/(.*)$': '/$1', '^~/(.*)$': '/$1', '^vue$': 'vue/dist/vue.common.js', diff --git a/tests/__mocks__/styleMock.js b/tests/__mocks__/styleMock.js new file mode 100644 index 0000000..4ba52ba --- /dev/null +++ b/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} From ca95ae023384bc8320de141a9a0e6be82ae88df3 Mon Sep 17 00:00:00 2001 From: Mehmet Baker Date: Thu, 3 Feb 2022 00:28:37 +0300 Subject: [PATCH 9/9] Added CompooundSuggestion.test.js (no tests yet) Signed-off-by: Mehmet Baker --- .gitignore | 2 ++ tests/components/CompoundSuggestion.test.js | 31 +++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/components/CompoundSuggestion.test.js diff --git a/.gitignore b/.gitignore index c77c515..cae0344 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,5 @@ ecosystem.config.js # SQL files db.sql + +.vscode/ diff --git a/tests/components/CompoundSuggestion.test.js b/tests/components/CompoundSuggestion.test.js new file mode 100644 index 0000000..010fab3 --- /dev/null +++ b/tests/components/CompoundSuggestion.test.js @@ -0,0 +1,31 @@ +import { createLocalVue } from '@vue/test-utils' +import { render } from '@testing-library/vue' +import Vuex from 'vuex' +// eslint-disable-next-line +import { __createMocks as createStoreMocks } from '../__mocks__/store' +import RightSideBar from '@/components/RightSideBar.vue' + +jest.mock('../../store') + +const localVue = createLocalVue() +localVue.use(Vuex) + +describe('Compound Suggestion', () => { + let storeMocks + + beforeEach(() => { + storeMocks = createStoreMocks() + render(RightSideBar, { + store: storeMocks.store, + localVue, + }) + }) + + it('parses compound formula', () => { + // TODO: Write the body + }) + + it('loads compound formula after clicking on a sugestion', () => { + // TODO: Write the body + }) +})