├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── deploy.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── jest.config.js ├── package.json ├── src ├── App.vue ├── components │ ├── AboutSection.vue │ ├── InputTips.vue │ ├── MainPage.vue │ ├── NormalForms.vue │ ├── SimplificationButton.vue │ ├── SimplificationSteps.vue │ └── WangHaoProof.vue ├── core │ ├── AstNode │ │ ├── AndNode.ts │ │ ├── AtomNode.ts │ │ ├── EqNode.ts │ │ ├── FalseNode.ts │ │ ├── ImpliedByNode.ts │ │ ├── ImplyNode.ts │ │ ├── NandNode.ts │ │ ├── NorNode.ts │ │ ├── NotNode.ts │ │ ├── OrNode.ts │ │ ├── TrueNode.ts │ │ ├── XorNode.ts │ │ ├── base.ts │ │ ├── index.ts │ │ └── precedence.ts │ ├── QuineMcCluskey │ │ ├── Minterm.ts │ │ ├── QuineMcCluskey.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── WangHao.ts │ ├── buildAst.ts │ ├── equivalents.ts │ ├── getTable.ts │ ├── transformExp.spec.ts │ └── transformExp.ts ├── main.ts ├── shims-vue.d.ts └── types │ ├── WangHaoTooLongError.ts │ ├── data.ts │ ├── equivalent.ts │ └── step.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | es2015: true, 7 | jest: true, 8 | }, 9 | extends: [ 10 | '@vue/typescript/recommended', 11 | 'plugin:vue/vue3-recommended', 12 | 'eslint:recommended', 13 | 'airbnb-base', 14 | ], 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | globals: { 20 | defineProps: 'readonly', 21 | defineEmits: 'readonly', 22 | defineExpose: 'readonly', 23 | withDefaults: 'readonly', 24 | }, 25 | rules: { 26 | 'import/extensions': 0, 27 | 'import/no-unresolved': 0, 28 | 'import/no-extraneous-dependencies': [ 29 | 'error', 30 | { 31 | devDependencies: [ 32 | './*.ts', 33 | ], 34 | }, 35 | ], 36 | 37 | semi: 0, 38 | '@typescript-eslint/semi': 2, 39 | 40 | '@typescript-eslint/explicit-module-boundary-types': 0, 41 | 42 | '@typescript-eslint/no-explicit-any': 0, 43 | 44 | '@typescript-eslint/type-annotation-spacing': 2, 45 | 46 | 'no-inner-declarations': 0, 47 | 48 | 'no-bitwise': 0, 49 | 50 | 'max-statements-per-line': ['error', { max: 1 }], 51 | 52 | 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], 53 | 54 | 'no-use-before-define': 0, 55 | '@typescript-eslint/no-use-before-define': 2, 56 | 57 | // for generic type parameters 58 | 'no-spaced-func': 0, 59 | 'func-call-spacing': 0, 60 | '@typescript-eslint/func-call-spacing': 2, 61 | 62 | // parameters with public/protected/private in class constructor 63 | 'no-unused-vars': 0, 64 | '@typescript-eslint/no-unused-vars': 2, 65 | 66 | indent: 0, 67 | '@typescript-eslint/indent': [ 68 | 'error', 69 | 2, 70 | ], 71 | 72 | 'vue/block-lang': ['error', 73 | { 74 | script: { 75 | lang: 'ts', 76 | }, 77 | }, 78 | ], 79 | 80 | 'vue/component-name-in-template-casing': ['error', 'kebab-case', { 81 | registeredComponentsOnly: false, 82 | }], 83 | 84 | 'vue/no-export-in-script-setup': 2, 85 | 86 | 'vue/no-reserved-component-names': ['error', { 87 | disallowVue3BuiltInComponents: true, 88 | }], 89 | 90 | 'vue/no-template-target-blank': 2, 91 | 92 | 'vue/no-unregistered-components': 2, 93 | 94 | 'vue/no-unused-refs': 2, 95 | 96 | 'vue/no-useless-mustaches': 2, 97 | 98 | 'vue/no-useless-v-bind': 2, 99 | 100 | 'vue/no-v-text': 2, 101 | 102 | 'vue/padding-line-between-blocks': 2, 103 | 104 | 'vue/v-for-delimiter-style': ['error', 'of'], 105 | 106 | 'vue/valid-define-emits': 2, 107 | 108 | 'vue/valid-define-props': 2, 109 | 110 | 'vue/valid-next-tick': 2, 111 | }, 112 | }; 113 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: 16 13 | cache: 'yarn' 14 | - run: yarn 15 | - run: yarn build 16 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 16 17 | cache: 'yarn' 18 | - run: yarn 19 | - run: yarn test 20 | - run: yarn build 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | personal_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_dir: dist 26 | publish_branch: dist 27 | force_orphan: true 28 | commit_message: ${{ github.event.head_commit.message }} 29 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: 16 13 | cache: 'yarn' 14 | - run: yarn 15 | - run: yarn lint:nofix 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: 16 13 | cache: 'yarn' 14 | - run: yarn 15 | - run: yarn test --coverage 16 | - uses: codecov/codecov-action@v2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /coverage 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Truth Table Generator 2 | 3 | 离散数学 Open Project。 4 | 5 | 它现在不仅有真值表,还有主范式、最简范式、王浩算法、手动等值演算。 6 | 7 | [Demo](https://ouuan.github.io/truth-table-generator) 8 | 9 | ```bash 10 | yarn 11 | yarn serve 12 | yarn build 13 | ``` 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Truth Table Generator - ouuan 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truth-table-generator", 3 | "version": "0.0.1", 4 | "description": "Truth table generator", 5 | "repository": "https://github.com/ouuan/truth-table-generator", 6 | "author": "Yufan You (https://github.com/ouuan)", 7 | "license": "Apache-2.0", 8 | "private": true, 9 | "scripts": { 10 | "lint": "yarn lint:nofix --fix", 11 | "lint:nofix": "eslint . --ignore-path .gitignore --ext .ts,.vue,.js", 12 | "lint:types": "vue-tsc --noEmit", 13 | "serve": "vite", 14 | "build": "yarn lint && yarn lint:types && vite build", 15 | "test": "jest" 16 | }, 17 | "dependencies": { 18 | "naive-ui": "^2.21.1", 19 | "ts-exhaustive-check": "^1.0.0", 20 | "vue": "^3.2.23" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^27.0.3", 24 | "@typescript-eslint/eslint-plugin": "^5.4.0", 25 | "@typescript-eslint/parser": "^5.4.0", 26 | "@vitejs/plugin-vue": "^1.10.1", 27 | "@vue/eslint-config-typescript": "^9.1.0", 28 | "eslint": "^8.3.0", 29 | "eslint-config-airbnb-base": "^15.0.0", 30 | "eslint-plugin-import": "^2.25.3", 31 | "eslint-plugin-vue": "^8.1.1", 32 | "jest": "^27.3.1", 33 | "ts-jest": "^27.0.7", 34 | "typescript": "^4.5.2", 35 | "vite": "^2.6.14", 36 | "vue-tsc": "^0.29.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/components/AboutSection.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /src/components/InputTips.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 51 | -------------------------------------------------------------------------------- /src/components/MainPage.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 232 | 233 | 243 | -------------------------------------------------------------------------------- /src/components/NormalForms.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 120 | -------------------------------------------------------------------------------- /src/components/SimplificationButton.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 86 | -------------------------------------------------------------------------------- /src/components/SimplificationSteps.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | -------------------------------------------------------------------------------- /src/components/WangHaoProof.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | 59 | 74 | -------------------------------------------------------------------------------- /src/core/AstNode/AndNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'and' as const; 6 | 7 | operator = '∧'; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | const r = this.children[1].dfsTruth(data); 16 | return this.children[0].dfsTruth(data) && r; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/AstNode/AtomNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | 3 | export default class extends AstNode { 4 | type = 'atom' as const; 5 | 6 | operator = ''; 7 | 8 | constructor(name: string) { 9 | super(); 10 | this.str = name; 11 | } 12 | 13 | // actually unused 14 | calc() { 15 | return !!this.type; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/AstNode/EqNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'eq' as const; 6 | 7 | operator = '↔'; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | return this.children[0].dfsTruth(data) === this.children[1].dfsTruth(data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/AstNode/FalseNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | 3 | export default class extends AstNode { 4 | type = 'false' as const; 5 | 6 | operator = ''; 7 | 8 | constructor() { 9 | super(); 10 | this.str = 'F'; 11 | } 12 | 13 | calc() { 14 | return !this.type; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/core/AstNode/ImpliedByNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'impliedby' as const; 6 | 7 | operator = '←' as const; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | const l = this.children[0].dfsTruth(data); 16 | return !this.children[1].dfsTruth(data) || l; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/AstNode/ImplyNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'imply' as const; 6 | 7 | operator = '→' as const; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | const r = this.children[1].dfsTruth(data); 16 | return !this.children[0].dfsTruth(data) || r; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/AstNode/NandNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'nand' as const; 6 | 7 | operator = '↑' as const; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | const r = this.children[1].dfsTruth(data); 16 | return !(this.children[0].dfsTruth(data) && r); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/AstNode/NorNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'nor' as const; 6 | 7 | operator = '↓' as const; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | const r = this.children[1].dfsTruth(data); 16 | return !(this.children[0].dfsTruth(data) || r); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/AstNode/NotNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'not' as const; 6 | 7 | operator = '¬'; 8 | 9 | constructor(internal: AstNode) { 10 | super(); 11 | this.children.push(internal); 12 | } 13 | 14 | calc(data: Data) { 15 | return !this.children[0].dfsTruth(data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/AstNode/OrNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'or' as const; 6 | 7 | operator = '∨'; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | const r = this.children[1].dfsTruth(data); 16 | return this.children[0].dfsTruth(data) || r; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/AstNode/TrueNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | 3 | export default class extends AstNode { 4 | type = 'true' as const; 5 | 6 | operator = ''; 7 | 8 | constructor() { 9 | super(); 10 | this.str = 'T'; 11 | } 12 | 13 | calc() { 14 | return !!this.type; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/core/AstNode/XorNode.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import Data from '~/types/data'; 3 | 4 | export default class extends AstNode { 5 | type = 'xor' as const; 6 | 7 | operator = '⊕' as const; 8 | 9 | constructor(left: AstNode, right: AstNode) { 10 | super(); 11 | this.children.push(left, right); 12 | } 13 | 14 | calc(data: Data) { 15 | return this.children[0].dfsTruth(data) !== this.children[1].dfsTruth(data); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/AstNode/base.ts: -------------------------------------------------------------------------------- 1 | import Data from '~/types/data'; 2 | 3 | import precedence from './precedence'; 4 | 5 | abstract class AstNode { 6 | abstract type: 'atom' | 'not' | 'and' | 'nand' | 'xor' | 'or' | 'nor' | 'imply' | 'impliedby' | 'eq' | 'true' | 'false'; 7 | 8 | abstract operator: string; 9 | 10 | children: AstNode[] = []; 11 | 12 | ch(index: number) { 13 | return this.children[index]; 14 | } 15 | 16 | static quote(u: AstNode, v: AstNode): string { 17 | if (precedence[u.type] < precedence[v.type] + 1) { 18 | return `(${v})`; 19 | } 20 | return v.toString(); 21 | } 22 | 23 | protected str = ''; 24 | 25 | updateStr() { 26 | this.children.forEach((v) => v.updateStr()); 27 | if (this.children.length === 1) { 28 | this.str = `${this.operator}${AstNode.quote(this, this.children[0])}`; 29 | } else if (this.children.length === 2) { 30 | this.str = `${AstNode.quote(this, this.children[0])} ${this.operator} ${AstNode.quote(this, this.children[1])}`; 31 | } 32 | } 33 | 34 | toString(): string { 35 | return this.str; 36 | } 37 | 38 | abstract calc(data: Data): boolean; 39 | 40 | dfsTruth(data: Data): boolean { 41 | if (data[this.str] === undefined) { 42 | /* eslint-disable no-param-reassign */ 43 | data[this.str] = this.calc(data) ? 1 : 0; 44 | } 45 | return !!data[this.str]; 46 | } 47 | } 48 | 49 | export default AstNode; 50 | -------------------------------------------------------------------------------- /src/core/AstNode/index.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './base'; 2 | import AtomNode from './AtomNode'; 3 | import NotNode from './NotNode'; 4 | import AndNode from './AndNode'; 5 | import NandNode from './NandNode'; 6 | import XorNode from './XorNode'; 7 | import OrNode from './OrNode'; 8 | import NorNode from './NorNode'; 9 | import ImplyNode from './ImplyNode'; 10 | import ImpliedByNode from './ImpliedByNode'; 11 | import EqNode from './EqNode'; 12 | import TrueNode from './TrueNode'; 13 | import FalseNode from './FalseNode'; 14 | 15 | export { 16 | AstNode, 17 | AtomNode, 18 | NotNode, 19 | AndNode, 20 | NandNode, 21 | XorNode, 22 | OrNode, 23 | NorNode, 24 | ImplyNode, 25 | ImpliedByNode, 26 | EqNode, 27 | TrueNode, 28 | FalseNode, 29 | }; 30 | -------------------------------------------------------------------------------- /src/core/AstNode/precedence.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | eq: 5, 3 | impliedby: 4.1, 4 | imply: 4, 5 | nor: 3.1, 6 | or: 3, 7 | xor: 2.5, 8 | nand: 2.1, 9 | and: 2, 10 | not: 1, 11 | atom: 0, 12 | true: 0, 13 | false: 0, 14 | } as const; 15 | -------------------------------------------------------------------------------- /src/core/QuineMcCluskey/Minterm.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A class to hold information about a minterm when using the Quine-McCluskey Algorithm 3 | */ 4 | export default class Minterm { 5 | values: number[]; 6 | 7 | value: string; 8 | 9 | used: boolean; 10 | 11 | /** 12 | * Creates a new minterm object 13 | */ 14 | constructor(values: number[], value: string) { 15 | this.values = values; 16 | this.value = value; 17 | this.used = false; 18 | 19 | this.values.sort(); 20 | } 21 | 22 | /** 23 | * Returns a String representation of the Minterm 24 | */ 25 | toString() { 26 | const values = this.values.join(', '); 27 | return `m(${values}) = ${this.value}`; 28 | } 29 | 30 | /** 31 | * Determines if this Minterm object is equal to another object 32 | */ 33 | equals(minterm: Minterm) { 34 | if (!(minterm instanceof Minterm)) { 35 | return false; 36 | } 37 | 38 | return ( 39 | this.value === minterm.value 40 | && this.values.length === minterm.values.length 41 | && this.values.every((u, i) => u === minterm.values[i]) 42 | ); 43 | } 44 | 45 | /** 46 | * Returns the values in this Minterm 47 | */ 48 | getValues() { 49 | return this.values; 50 | } 51 | 52 | /** 53 | * Returns the binary value of this Minterm 54 | */ 55 | getValue() { 56 | return this.value; 57 | } 58 | 59 | /** 60 | * Returns whether or not this Minterm has been used 61 | */ 62 | isUsed() { 63 | return this.used; 64 | } 65 | 66 | /** 67 | * Labels this Minterm as "used" 68 | */ 69 | use() { 70 | this.used = true; 71 | } 72 | 73 | /** 74 | * Combines 2 Minterms together if they can be combined 75 | */ 76 | combine(minterm: Minterm) { 77 | // Check if this value is this same; If so, do nothing 78 | if (this.value === minterm.value) { 79 | return null; 80 | } 81 | 82 | // Check if the values are the same; If so, do nothing 83 | if ( 84 | this.values.length === minterm.values.length 85 | && this.values.every((u, i) => u === minterm.values[i]) 86 | ) { 87 | return null; 88 | } 89 | 90 | // Keep track of the difference between the minterms 91 | let diff = 0; 92 | let result = ''; 93 | 94 | // Iterate through the bits in this Minterm's value 95 | for (let i = 0; i < this.value.length; i += 1) { 96 | // Check if the current bit value differs from the minterm's bit value 97 | if (this.value.charAt(i) !== minterm.value.charAt(i)) { 98 | diff += 1; 99 | result += '-'; 100 | } else { 101 | // There is no difference 102 | result += this.value.charAt(i); 103 | } 104 | 105 | // The difference has exceeded 1 106 | if (diff > 1) { 107 | return null; 108 | } 109 | } 110 | 111 | return new Minterm(this.values.concat(minterm.values), result); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/core/QuineMcCluskey/QuineMcCluskey.ts: -------------------------------------------------------------------------------- 1 | import Minterm from './Minterm'; 2 | import { decToBin, valueIn } from './utils'; 3 | 4 | /** 5 | * A class to handle processing the Quine-McCluskey Algorithm 6 | */ 7 | export default class QuineMcCluskey { 8 | variables: string; 9 | 10 | values: number[]; 11 | 12 | allValues: number[]; 13 | 14 | dontCares: number[]; 15 | 16 | isMaxterm: boolean; 17 | 18 | func: string; 19 | 20 | /** 21 | * Creates a new QuineMcCluskey object to process the Quine-Mccluskey Algorithm 22 | */ 23 | constructor( 24 | variables: string, 25 | values: number[], 26 | dontCares: number[] = [], 27 | isMaxterm = false, 28 | ) { 29 | values.sort(); 30 | this.variables = variables; 31 | this.values = values; 32 | this.allValues = values.concat(dontCares); 33 | this.allValues.sort(); 34 | this.dontCares = dontCares; 35 | this.isMaxterm = isMaxterm; 36 | this.func = this.getFunction() || ''; 37 | } 38 | 39 | // Helper Methods 40 | 41 | /** 42 | * Returns the binary value equivalent to the decimal value given 43 | */ 44 | getBits(value: number) { 45 | let s = (value >>> 0).toString(2); 46 | for (let i = s.length; i < this.variables.length; i += 1) s = `0${s}`; 47 | return s; 48 | } 49 | 50 | // Grouping Methods 51 | 52 | /** 53 | * Creates the initial grouping for the bits from the values 54 | * given to the Quine-McCluskey Algorithm 55 | */ 56 | initialGroup() { 57 | // Keep track of groups by 2-dimensional array 58 | const groups: Minterm[][] = []; 59 | for (let i = 0; i < this.variables.length + 1; i += 1) { 60 | groups.push([]); 61 | } 62 | 63 | // Iterate through values 64 | for (const value of this.allValues) { 65 | // Count number of 1's in value's bit equivalent 66 | let count = 0; 67 | const bits = this.getBits(value); 68 | for (const bit of bits) { 69 | if (bit === '1') { 70 | count += 1; 71 | } 72 | } 73 | 74 | // Add count to proper group 75 | groups[count].push(new Minterm([value], bits)); 76 | } 77 | 78 | return groups; 79 | } 80 | 81 | /** 82 | * Creates a power set of all valid prime implicants that covers the rest of an expression. 83 | * This is used after the essential prime implicants have been found. 84 | */ 85 | static powerSet(values: number[], primeImplicants: Minterm[]) { 86 | // Get the power set of all the prime implicants 87 | let powerset = []; 88 | 89 | // Iterate through decimal values from 1 to 2 ** size - 1 90 | for (let i = 1; i < 2 ** primeImplicants.length - 1; i += 1) { 91 | const currentset = []; 92 | 93 | // Get the binary value of the decimal value 94 | let binValue = decToBin(i); 95 | for (let j = binValue.length; j < primeImplicants.length; j += 1) { 96 | binValue = `0${binValue}`; 97 | } 98 | 99 | // Find which indexes have 1 in the binValue string 100 | for (let j = 0; j < binValue.length; j += 1) { 101 | if (binValue.charAt(j) === '1') { 102 | currentset.push(primeImplicants[j]); 103 | } 104 | } 105 | powerset.push(currentset); 106 | } 107 | 108 | // Remove all subsets that do not cover the rest of the implicants 109 | const newpowerset = []; 110 | for (const subset of powerset) { 111 | // Get all the values the set covers 112 | const tempValues = []; 113 | for (const implicant of subset) { 114 | for (const value of implicant.getValues()) { 115 | if (!valueIn(value, tempValues) && valueIn(value, values)) { 116 | tempValues.push(value); 117 | } 118 | } 119 | } 120 | tempValues.sort(); 121 | 122 | // Check if this subset covers the rest of the values 123 | if ( 124 | tempValues.length === values.length 125 | && tempValues.every((u, i) => u === values[i]) 126 | ) { 127 | newpowerset.push(subset); 128 | } 129 | } 130 | powerset = newpowerset; 131 | 132 | // Find the minimum amount of implicants that can cover the expression 133 | let minSet = powerset[0]; 134 | for (const subset of powerset) { 135 | if (subset.length < minSet.length) { 136 | minSet = subset; 137 | } 138 | } 139 | 140 | if (minSet === undefined) { 141 | return []; 142 | } 143 | return minSet; 144 | } 145 | 146 | // Compare Methods 147 | 148 | /** 149 | * Returns an array of all the prime implicants for an expression 150 | */ 151 | getPrimeImplicants(groups = this.initialGroup()) { 152 | // If there is only 1 group, return all the minterms in it 153 | if (groups.length === 1) { 154 | return groups[0]; 155 | } 156 | 157 | // Try comparing the rest 158 | 159 | const unused: Minterm[] = []; 160 | const comparisons = [...Array(groups.length - 1).keys()]; 161 | const newGroups: Minterm[][] = comparisons.map(() => []); 162 | 163 | // Compare each adjacent group 164 | for (const compare of comparisons) { 165 | const group1 = groups[compare]; 166 | const group2 = groups[compare + 1]; 167 | 168 | // Compare every term in group1 with every term in group2 169 | for (const term1 of group1) { 170 | for (const term2 of group2) { 171 | // Try combining it 172 | const term3 = term1.combine(term2); 173 | 174 | // Only add it to the new group if term3 is not null 175 | // term3 will only be null if term1 and term2 could not 176 | // be combined 177 | if (term3 !== null) { 178 | term1.use(); 179 | term2.use(); 180 | if (!valueIn(term3, newGroups[compare])) { 181 | newGroups[compare].push(term3); 182 | } 183 | } 184 | } 185 | } 186 | } 187 | 188 | // Get array of all unused minterms 189 | for (const group of groups) { 190 | for (const term of group) { 191 | if (!term.isUsed() && !valueIn(term, unused)) { 192 | unused.push(term); 193 | } 194 | } 195 | } 196 | 197 | // Add recursive call 198 | for (const term of this.getPrimeImplicants(newGroups)) { 199 | if (!term.isUsed() && !valueIn(term, unused)) { 200 | unused.push(term); 201 | } 202 | } 203 | 204 | return unused; 205 | } 206 | 207 | // Solving Methods 208 | 209 | /** 210 | * Solves for the expression returning the minimal amount of prime implicants needed 211 | * to cover the expression. 212 | */ 213 | solve() { 214 | // Get the prime implicants 215 | let primeImplicants = this.getPrimeImplicants(); 216 | 217 | // Keep track of values with only 1 implicant 218 | // These are the essential prime implicants 219 | const essentialPrimeImplicants = []; 220 | const valuesUsed = []; 221 | for (let i = 0; i < this.values.length; i += 1) { 222 | valuesUsed.push(false); 223 | } 224 | 225 | // Iterate through values 226 | for (let i = 0; i < this.values.length; i += 1) { 227 | const value = this.values[i]; 228 | 229 | let uses = 0; 230 | let last = null; 231 | for (const minterm of primeImplicants) { 232 | if (valueIn(value, minterm.getValues())) { 233 | uses += 1; 234 | last = minterm; 235 | } 236 | } 237 | if (uses === 1 && last && !valueIn(last, essentialPrimeImplicants)) { 238 | for (const v of last.getValues()) { 239 | if (!valueIn(v, this.dontCares)) { 240 | valuesUsed[this.values.indexOf(v)] = true; 241 | } 242 | } 243 | essentialPrimeImplicants.push(last); 244 | } 245 | } 246 | 247 | // Check if all values were used 248 | let found = false; 249 | for (const value of valuesUsed) { 250 | if (!value) { 251 | found = true; 252 | break; 253 | } 254 | } 255 | if (!found) { 256 | return essentialPrimeImplicants; 257 | } 258 | 259 | // Keep track of prime implicants that cover as many values as possible 260 | // with as few variables as possible 261 | const newPrimeImplicants = []; 262 | for (const implicant of primeImplicants) { 263 | if (!valueIn(implicant, essentialPrimeImplicants)) { 264 | // Check if the current implicant only consists of dont cares 265 | let add = false; 266 | for (const value of implicant.getValues()) { 267 | if (!valueIn(value, this.dontCares)) { 268 | add = true; 269 | break; 270 | } 271 | } 272 | if (add) { 273 | newPrimeImplicants.push(implicant); 274 | } 275 | } 276 | } 277 | primeImplicants = newPrimeImplicants; 278 | 279 | // Check if there is only 1 implicant left (very rare but just in case) 280 | if (primeImplicants.length === 1) { 281 | return essentialPrimeImplicants.concat(primeImplicants); 282 | } 283 | 284 | // Create a power set from the remaining prime implicants and check which 285 | // combination of prime implicants gets the simplest form 286 | const newValues = []; 287 | for (let i = 0; i < this.values.length; i += 1) { 288 | if (!valuesUsed[i]) { 289 | newValues.push(this.values[i]); 290 | } 291 | } 292 | 293 | return essentialPrimeImplicants.concat( 294 | QuineMcCluskey.powerSet(newValues, primeImplicants), 295 | ); 296 | } 297 | 298 | /** 299 | * Returns the expression in a readable form 300 | */ 301 | getFunction() { 302 | // Check if function already exists, return it 303 | if (this.func != null) { 304 | return this.func; 305 | } 306 | 307 | // Get the prime implicants and variables 308 | const primeImplicants = this.solve(); 309 | 310 | // Check if there are no prime implicants; Always False 311 | if (primeImplicants.length === 0) { 312 | return this.isMaxterm ? 'T' : 'F'; 313 | } 314 | 315 | if (primeImplicants.length === 1) { 316 | let count = 0; 317 | for (const index of primeImplicants[0].getValue()) { 318 | if (index === '-') { 319 | count += 1; 320 | } 321 | } 322 | if (count === this.variables.length) { 323 | return this.isMaxterm ? 'F' : 'T'; 324 | } 325 | } 326 | 327 | let result = ''; 328 | 329 | // Iterate through the prime implicants 330 | for (let i = 0; i < primeImplicants.length; i += 1) { 331 | const implicant = primeImplicants[i]; 332 | 333 | // Add parentheses if necessary 334 | if ( 335 | (implicant.getValue().match(/-/g) || []).length 336 | < this.variables.length - 1 337 | ) { 338 | result += '('; 339 | } 340 | 341 | // Iterate through all the bits in the implicants value 342 | for (let j = 0; j < implicant.getValue().length; j += 1) { 343 | if (implicant.getValue().charAt(j) === (this.isMaxterm ? '1' : '0')) { 344 | result += '¬'; 345 | } 346 | if (implicant.getValue().charAt(j) !== '-') { 347 | result += this.variables[j]; 348 | } 349 | if ( 350 | ( 351 | implicant 352 | .getValue() 353 | .substring(j + 1) 354 | .match(/-/g) || [] 355 | ).length 356 | < implicant.getValue().length - j - 1 357 | && implicant.getValue().charAt(j) !== '-' 358 | ) { 359 | result += this.isMaxterm ? ' ∨ ' : ' ∧ '; 360 | } 361 | } 362 | 363 | // Add parentheses if necessary 364 | if ( 365 | (implicant.getValue().match(/-/g) || []).length 366 | < this.variables.length - 1 367 | ) { 368 | result += ')'; 369 | } 370 | 371 | // Combine minterms with an OR operator 372 | if (i < primeImplicants.length - 1) { 373 | result += this.isMaxterm ? ' ∧ ' : ' ∨ '; 374 | } 375 | } 376 | 377 | return result; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/core/QuineMcCluskey/index.ts: -------------------------------------------------------------------------------- 1 | import QuineMcCluskey from './QuineMcCluskey'; 2 | 3 | export default QuineMcCluskey; 4 | -------------------------------------------------------------------------------- /src/core/QuineMcCluskey/utils.ts: -------------------------------------------------------------------------------- 1 | import Minterm from './Minterm'; 2 | 3 | /** 4 | * Converts a value from decimal to binary 5 | * 6 | * @param value The value to convert to binary 7 | */ 8 | export function decToBin(value: number) { 9 | return (value >>> 0).toString(2); 10 | } 11 | 12 | /** 13 | * Returns whether or not a value is in an array 14 | * 15 | * @param value The value to look for in an array 16 | * @param array The array to look for a value in 17 | */ 18 | export function valueIn(value: number | Minterm, array: (number|Minterm)[]) { 19 | return Boolean(array.find((item) => { 20 | if (value instanceof Minterm && item instanceof Minterm) { 21 | return value.equals(item); 22 | } 23 | return item === value; 24 | })); 25 | } 26 | -------------------------------------------------------------------------------- /src/core/WangHao.ts: -------------------------------------------------------------------------------- 1 | import { exhaustiveCheck } from 'ts-exhaustive-check'; 2 | import { NText, TreeOption } from 'naive-ui'; 3 | import { h } from 'vue'; 4 | 5 | import { AstNode } from './AstNode'; 6 | import equivalents from './equivalents'; 7 | import WangHaoTooLongError from '~/types/WangHaoTooLongError'; 8 | 9 | interface WangHaoProof extends TreeOption { 10 | key: string; 11 | label: string; 12 | isTrue: boolean; 13 | children?: WangHaoProof[]; 14 | } 15 | 16 | export default class WangHao { 17 | private left = new Set(); 18 | 19 | private right = new Set(); 20 | 21 | private leftStr = new Set(); 22 | 23 | private rightStr = new Set(); 24 | 25 | private isTrue = false; 26 | 27 | private tot: { value: number }; 28 | 29 | key: number; 30 | 31 | private rule = ''; 32 | 33 | constructor(root: AstNode); 34 | 35 | constructor(old: WangHao); 36 | 37 | constructor(x: AstNode | WangHao) { 38 | if (x instanceof AstNode) { 39 | this.leftStr.add('T'); 40 | this.rightStr.add('F'); 41 | this.rightStr.add(x.toString()); 42 | if (x.toString().length > 1) { 43 | this.right.add(x); 44 | } 45 | if (x.type === 'true') { 46 | this.isTrue = true; 47 | this.rule = 'T ⇒ T'; 48 | } 49 | this.key = 1; 50 | this.tot = { value: 1 }; 51 | } else { 52 | for (const l of x.left) this.left.add(l); 53 | for (const r of x.right) this.right.add(r); 54 | for (const l of x.leftStr) this.leftStr.add(l); 55 | for (const r of x.rightStr) this.rightStr.add(r); 56 | this.tot = x.tot; 57 | this.tot.value += 1; 58 | if (this.tot.value > 1e4) throw new WangHaoTooLongError(); 59 | this.key = x.tot.value; 60 | } 61 | } 62 | 63 | toString(): string { 64 | const lItems = []; 65 | const rItems = []; 66 | 67 | for (const l of this.leftStr.values()) { 68 | if (l !== 'T') { 69 | lItems.push(l); 70 | } 71 | } 72 | if (lItems.length === 0) lItems.push('T'); 73 | 74 | for (const r of this.rightStr.values()) { 75 | if (r !== 'F') { 76 | rItems.push(r); 77 | } 78 | } 79 | if (rItems.length === 0) rItems.push('F'); 80 | 81 | return `${lItems.join(', ')} ⇒ ${rItems.join(', ')}`; 82 | } 83 | 84 | private del(node: AstNode, side: 'left' | 'right') { 85 | this[side].delete(node); 86 | this[`${side}Str`].delete(node.toString()); 87 | } 88 | 89 | private add(node: AstNode, side: 'left' | 'right') { 90 | if (this[`${side}Str`].has(node.toString())) return; 91 | this[`${side}Str`].add(node.toString()); 92 | if (node.toString().length > 1) this[side].add(node); 93 | if (this[side === 'left' ? 'rightStr' : 'leftStr'].has(node.toString())) { 94 | this.isTrue = true; 95 | this.rule = `${node} ⇒ ${node}`; 96 | } 97 | } 98 | 99 | solve(): WangHaoProof { 100 | const children: WangHaoProof[] = []; 101 | 102 | if (!this.isTrue) { 103 | if (this.left.size > 0) { 104 | const node: AstNode = this.left.values().next().value; 105 | 106 | const a = new WangHao(this); 107 | a.del(node, 'left'); 108 | 109 | this.rule = `${node.operator} ⇒`; 110 | 111 | switch (node.type) { 112 | case 'not': 113 | { 114 | a.add(node.ch(0), 'right'); 115 | children.push(a.solve()); 116 | break; 117 | } 118 | case 'and': 119 | { 120 | a.add(node.ch(0), 'left'); 121 | a.add(node.ch(1), 'left'); 122 | children.push(a.solve()); 123 | break; 124 | } 125 | case 'or': 126 | { 127 | const b = new WangHao(a); 128 | a.add(node.ch(0), 'left'); 129 | b.add(node.ch(1), 'left'); 130 | children.push(a.solve(), b.solve()); 131 | break; 132 | } 133 | case 'imply': 134 | { 135 | const b = new WangHao(a); 136 | a.add(node.ch(1), 'left'); 137 | b.add(node.ch(0), 'right'); 138 | children.push(a.solve(), b.solve()); 139 | break; 140 | } 141 | case 'eq': 142 | { 143 | const b = new WangHao(a); 144 | a.add(node.ch(0), 'left'); 145 | a.add(node.ch(1), 'left'); 146 | b.add(node.ch(0), 'right'); 147 | b.add(node.ch(1), 'right'); 148 | children.push(a.solve(), b.solve()); 149 | break; 150 | } 151 | case 'nor': 152 | case 'xor': 153 | case 'nand': 154 | case 'impliedby': 155 | { 156 | const { result } = equivalents(node)[0]; 157 | result.updateStr(); 158 | a.add(result, 'left'); 159 | children.push(a.solve()); 160 | break; 161 | } 162 | case 'atom': 163 | case 'true': 164 | case 'false': 165 | throw Error(`${node.type} appears in WangHao's left!`); 166 | default: 167 | exhaustiveCheck(node.type); 168 | } 169 | } else if (this.right.size > 0) { 170 | const node: AstNode = this.right.values().next().value; 171 | 172 | const a = new WangHao(this); 173 | a.del(node, 'right'); 174 | 175 | this.rule = `⇒ ${node.operator}`; 176 | 177 | switch (node.type) { 178 | case 'not': 179 | { 180 | a.add(node.ch(0), 'left'); 181 | children.push(a.solve()); 182 | break; 183 | } 184 | case 'and': 185 | { 186 | const b = new WangHao(a); 187 | a.add(node.ch(0), 'right'); 188 | b.add(node.ch(1), 'right'); 189 | children.push(a.solve(), b.solve()); 190 | break; 191 | } 192 | case 'or': 193 | { 194 | a.add(node.ch(0), 'right'); 195 | a.add(node.ch(1), 'right'); 196 | children.push(a.solve()); 197 | break; 198 | } 199 | case 'imply': 200 | { 201 | a.add(node.ch(0), 'left'); 202 | a.add(node.ch(1), 'right'); 203 | children.push(a.solve()); 204 | break; 205 | } 206 | case 'eq': 207 | { 208 | const b = new WangHao(a); 209 | a.add(node.ch(0), 'left'); 210 | a.add(node.ch(1), 'right'); 211 | b.add(node.ch(0), 'right'); 212 | b.add(node.ch(1), 'left'); 213 | children.push(a.solve(), b.solve()); 214 | break; 215 | } 216 | case 'nor': 217 | case 'xor': 218 | case 'nand': 219 | case 'impliedby': 220 | { 221 | const { result } = equivalents(node)[0]; 222 | result.updateStr(); 223 | a.add(result, 'right'); 224 | children.push(a.solve()); 225 | break; 226 | } 227 | case 'atom': 228 | case 'true': 229 | case 'false': 230 | throw Error(`${node.type} appears in WangHao's right!`); 231 | default: 232 | exhaustiveCheck(node.type); 233 | } 234 | } 235 | 236 | if (children.length) { 237 | this.isTrue = true; 238 | children.forEach(({ isTrue }) => { 239 | if (!isTrue) { 240 | this.isTrue = false; 241 | } 242 | }); 243 | } 244 | } 245 | 246 | return { 247 | key: `${Number(this.isTrue)}-${this.key}-wh-${new Date().valueOf()}`, 248 | label: this.toString(), 249 | isTrue: this.isTrue, 250 | children: children.length === 0 ? undefined : children, 251 | isLeaf: children.length === 0, 252 | suffix: () => h(NText, { 253 | type: 'info', 254 | class: 'wanghao-rule', 255 | }, { 256 | default: () => (this.rule ? this.rule : undefined), 257 | }), 258 | }; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/core/buildAst.ts: -------------------------------------------------------------------------------- 1 | import { exhaustiveCheck } from 'ts-exhaustive-check'; 2 | import { 3 | AstNode, 4 | AtomNode, 5 | NotNode, 6 | AndNode, 7 | NandNode, 8 | XorNode, 9 | OrNode, 10 | NorNode, 11 | ImplyNode, 12 | ImpliedByNode, 13 | EqNode, 14 | TrueNode, 15 | FalseNode, 16 | } from './AstNode'; 17 | 18 | import transformExp from './transformExp'; 19 | 20 | const precedence = { 21 | '=': 5, 22 | '<': 4.1, 23 | '>': 4, 24 | '↓': 3.1, 25 | '|': 3, 26 | '^': 2.5, 27 | '↑': 2.1, 28 | '&': 2, 29 | } as const; 30 | 31 | export default function buildAst(expression: string): { 32 | root: AstNode, 33 | atomNodes: Map 34 | } | null { 35 | try { 36 | const exp = transformExp(expression); 37 | 38 | if (/[^A-Z()!&|><=↑^↓]/.test(exp)) return null; 39 | 40 | const stacks: (AstNode | '&' | '↑' | '^' | '↓' | '|' | '>' | '<' | '=' | '!')[][] = [[]]; 41 | 42 | function current() { 43 | return stacks[stacks.length - 1]; 44 | } 45 | 46 | function isBinaryOperator(x: unknown): x is '&' | '↑' | '^' | '↓' | '|' | '>' | '<' | '=' { 47 | return typeof x === 'string' && /^[&↑^↓|><=]$/.test(x); 48 | } 49 | 50 | function push(x: (typeof stacks)[number][number]) { 51 | if (current().length === 0) { 52 | current().push(x); 53 | return; 54 | } 55 | const last = current()[current().length - 1]; 56 | function error() { 57 | return `Invalid expression: "${x}" after "${last}"`; 58 | } 59 | if (last === '!') { 60 | if (isBinaryOperator(x)) throw error(); 61 | if (x === '!') current().push('!'); 62 | else { 63 | current().pop(); 64 | push(new NotNode(x)); 65 | } 66 | } else if (isBinaryOperator(last)) { 67 | if (isBinaryOperator(x)) throw error(); 68 | current().push(x); 69 | } else if (isBinaryOperator(x)) current().push(x); 70 | else throw error(); 71 | } 72 | 73 | function reduce() { 74 | const error = new Error('Reduce error'); 75 | 76 | const right = current().pop(); 77 | if (typeof right !== 'object') throw error; 78 | 79 | const operator = current().pop(); 80 | if (typeof operator !== 'string') throw error; 81 | 82 | const left = current().pop(); 83 | if (typeof left !== 'object') throw error; 84 | 85 | switch (operator) { 86 | case '&': 87 | push(new AndNode(left, right)); 88 | break; 89 | case '↑': 90 | push(new NandNode(left, right)); 91 | break; 92 | case '^': 93 | push(new XorNode(left, right)); 94 | break; 95 | case '↓': 96 | push(new NorNode(left, right)); 97 | break; 98 | case '|': 99 | push(new OrNode(left, right)); 100 | break; 101 | case '>': 102 | push(new ImplyNode(left, right)); 103 | break; 104 | case '<': 105 | push(new ImpliedByNode(left, right)); 106 | break; 107 | case '=': 108 | push(new EqNode(left, right)); 109 | break; 110 | case '!': 111 | throw error; 112 | default: 113 | exhaustiveCheck(operator); 114 | } 115 | } 116 | 117 | const atomNodes: Map = new Map(); 118 | 119 | for (let pos = 0; pos < exp.length; pos += 1) { 120 | const c = exp[pos]; 121 | switch (c) { 122 | case '(': 123 | stacks.push([]); 124 | break; 125 | case ')': { 126 | while (current().length > 1) reduce(); 127 | const res = current()[0]; 128 | if (typeof res !== 'object') throw Error('End bracket error'); 129 | stacks.pop(); 130 | push(res); 131 | break; 132 | } 133 | case '&': 134 | case '↑': 135 | case '^': 136 | case '↓': 137 | case '|': 138 | case '>': 139 | case '<': 140 | case '=': 141 | { 142 | while (current().length > 1) { 143 | const operator = current()[current().length - 2]; 144 | if (typeof operator !== 'string' || operator === '!') throw Error('Invalid stack'); 145 | if (precedence[operator] < precedence[c] || (operator === c && c !== '>')) reduce(); 146 | else break; 147 | } 148 | push(c); 149 | break; 150 | } 151 | case '!': 152 | push('!'); 153 | break; 154 | case 'T': 155 | push(new TrueNode()); 156 | break; 157 | case 'F': 158 | push(new FalseNode()); 159 | break; 160 | default: { 161 | const node = new AtomNode(c); 162 | let nodes = atomNodes.get(c); 163 | if (!nodes) { 164 | nodes = []; 165 | atomNodes.set(c, nodes); 166 | } 167 | nodes.push(node); 168 | push(node); 169 | break; 170 | } 171 | } 172 | } 173 | 174 | if (stacks.length !== 1) throw Error('Bracket error'); 175 | 176 | while (current().length > 1) reduce(); 177 | 178 | const root = current()[0]; 179 | if (typeof root === 'string') throw Error('Invalid root'); 180 | 181 | root.updateStr(); 182 | 183 | return { 184 | root, 185 | atomNodes, 186 | }; 187 | } catch (error) { 188 | return null; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/core/equivalents.ts: -------------------------------------------------------------------------------- 1 | // Ideally this should be implemented in derived classes, 2 | // but that could lead to cyclic imports, which is tricky to resolve 3 | 4 | import { exhaustiveCheck } from 'ts-exhaustive-check'; 5 | import Equivalent from '~/types/equivalent'; 6 | import { 7 | AndNode, 8 | AstNode, 9 | EqNode, 10 | FalseNode, 11 | ImplyNode, 12 | NotNode, 13 | OrNode, 14 | TrueNode, 15 | } from './AstNode'; 16 | 17 | export default function equivalents(node: AstNode): Equivalent[] { 18 | const result: Equivalent[] = []; 19 | 20 | switch (node.type) { 21 | case 'and': 22 | if (node.ch(0).type === node.type) { 23 | result.push({ 24 | name: '结合律', 25 | result: new AndNode(node.ch(0).ch(0), new AndNode(node.ch(0).ch(1), node.ch(1))), 26 | }); 27 | } 28 | if (node.ch(1).type === node.type) { 29 | result.push({ 30 | name: '结合律', 31 | result: new AndNode(new AndNode(node.ch(0), node.ch(1).ch(0)), node.ch(1).ch(1)), 32 | }); 33 | } 34 | result.push({ 35 | name: '交换律', 36 | result: new AndNode(node.ch(1), node.ch(0)), 37 | }); 38 | if (node.ch(0).type === 'or' && node.ch(1).type === 'or' && node.ch(0).ch(0).toString() === node.ch(1).ch(0).toString()) { 39 | result.push({ 40 | name: '分配律', 41 | result: new OrNode(node.ch(0).ch(0), new AndNode(node.ch(0).ch(1), node.ch(1).ch(1))), 42 | }); 43 | } 44 | if (node.ch(1).type === 'or') { 45 | result.push({ 46 | name: '分配律', 47 | result: new OrNode( 48 | new AndNode(node.ch(0), node.ch(1).ch(0)), 49 | new AndNode(node.ch(0), node.ch(1).ch(1)), 50 | ), 51 | }); 52 | } 53 | if (node.ch(0).toString() === node.ch(1).toString()) { 54 | result.push({ 55 | name: '等幂律', 56 | result: node.ch(0), 57 | }); 58 | } 59 | if (node.ch(1).type === 'or' && node.ch(0).toString() === node.ch(1).ch(0).toString()) { 60 | result.push({ 61 | name: '吸收律', 62 | result: node.ch(0), 63 | }); 64 | } 65 | if (node.ch(0).type === 'not' && node.ch(1).type === 'not') { 66 | result.push({ 67 | name: '摩根律', 68 | result: new NotNode(new OrNode(node.ch(0).ch(0), node.ch(1).ch(0))), 69 | }); 70 | } 71 | if (node.ch(1).type === 'not') { 72 | result.push({ 73 | name: '摩根律', 74 | result: new NotNode(new ImplyNode(node.ch(0), node.ch(1).ch(0))), 75 | }); 76 | } 77 | if (node.ch(1).type === 'true') { 78 | result.push({ 79 | name: '同一律', 80 | result: node.ch(0), 81 | }); 82 | } 83 | if (node.ch(1).type === 'false') { 84 | result.push({ 85 | name: '零律', 86 | result: node.ch(1), 87 | }); 88 | } 89 | if (node.ch(1).type === 'not' && node.ch(0).toString() === node.ch(1).ch(0).toString()) { 90 | result.push({ 91 | name: '补余律', 92 | result: new FalseNode(), 93 | }); 94 | } 95 | if (node.ch(0).type === 'imply' && node.ch(1).type === 'imply' && node.ch(0).ch(1).toString() === node.ch(1).ch(1).toString()) { 96 | result.push({ 97 | name: '前提析取合并', 98 | result: new ImplyNode(new OrNode(node.ch(0).ch(0), node.ch(1).ch(0)), node.ch(0).ch(1)), 99 | }); 100 | } 101 | if (node.ch(0).type === 'or' && node.ch(1).type === 'or' 102 | && node.ch(0).ch(1).type === 'not' && node.ch(1).ch(0).type === 'not' 103 | && node.ch(0).ch(0).toString() === node.ch(1).ch(0).ch(0).toString() 104 | && node.ch(0).ch(1).ch(0).toString() === node.ch(1).ch(1).toString()) { 105 | result.push({ 106 | name: '从取假来描述双条件', 107 | result: new EqNode(node.ch(0).ch(0), node.ch(1).ch(1)), 108 | }); 109 | } 110 | if (node.ch(0).type === 'imply' && node.ch(1).type === 'imply' 111 | && node.ch(0).ch(0).toString() === node.ch(1).ch(1).toString() 112 | && node.ch(0).ch(1).toString() === node.ch(1).ch(0).toString()) { 113 | result.push({ 114 | name: '等价等值式', 115 | result: new EqNode(node.ch(0).ch(0), node.ch(0).ch(1)), 116 | }); 117 | } 118 | if (node.ch(0).type === 'imply' && node.ch(1).type === 'imply' && node.ch(1).ch(1).type === 'not' 119 | && node.ch(0).ch(0).toString() === node.ch(1).ch(0).toString() 120 | && node.ch(0).ch(1).toString() === node.ch(1).ch(1).ch(0).toString()) { 121 | result.push({ 122 | name: '归谬论', 123 | result: new NotNode(node.ch(0).ch(0)), 124 | }); 125 | } 126 | break; 127 | case 'atom': 128 | break; 129 | case 'eq': 130 | if (node.ch(0).type === node.type) { 131 | result.push({ 132 | name: '结合律', 133 | result: new EqNode(node.ch(0).ch(0), new EqNode(node.ch(0).ch(1), node.ch(1))), 134 | }); 135 | } 136 | if (node.ch(1).type === node.type) { 137 | result.push({ 138 | name: '结合律', 139 | result: new EqNode(new EqNode(node.ch(0), node.ch(1).ch(0)), node.ch(1).ch(1)), 140 | }); 141 | } 142 | result.push({ 143 | name: '交换律', 144 | result: new EqNode(node.ch(1), node.ch(0)), 145 | }); 146 | if (node.ch(0).toString() === node.ch(1).toString()) { 147 | result.push({ 148 | name: '等幂律', 149 | result: new TrueNode(), 150 | }); 151 | } 152 | if (node.ch(0).type === 'true') { 153 | result.push({ 154 | name: '同一律', 155 | result: node.ch(1), 156 | }); 157 | } 158 | if (node.ch(0).type === 'false') { 159 | result.push({ 160 | name: '同一律', 161 | result: new NotNode(node.ch(1)), 162 | }); 163 | } 164 | if (node.ch(1).type === 'not' && node.ch(0).toString() === node.ch(1).ch(0).toString()) { 165 | result.push({ 166 | name: '补余律', 167 | result: new FalseNode(), 168 | }); 169 | } 170 | result.push({ 171 | name: '从取真来描述双条件', 172 | result: new OrNode( 173 | new AndNode(node.ch(0), node.ch(1)), 174 | new AndNode(new NotNode(node.ch(0)), new NotNode(node.ch(1))), 175 | ), 176 | }, { 177 | name: '从取假来描述双条件', 178 | result: new AndNode( 179 | new OrNode(node.ch(0), new NotNode(node.ch(1))), 180 | new OrNode(new NotNode(node.ch(0)), node.ch(1)), 181 | ), 182 | }, { 183 | name: '等价等值式', 184 | result: new AndNode( 185 | new ImplyNode(node.ch(0), node.ch(1)), 186 | new ImplyNode(node.ch(1), node.ch(0)), 187 | ), 188 | }, { 189 | name: '等价否定等值式', 190 | result: new EqNode(new NotNode(node.ch(0)), new NotNode(node.ch(1))), 191 | }); 192 | if (node.ch(0).type === 'not' && node.ch(1).type === 'not') { 193 | result.push({ 194 | name: '等价否定等值式', 195 | result: new EqNode(node.ch(0).ch(0), node.ch(1).ch(0)), 196 | }); 197 | } 198 | break; 199 | case 'false': 200 | result.push({ 201 | name: 'F = ¬T', 202 | result: new NotNode(new TrueNode()), 203 | }); 204 | break; 205 | case 'impliedby': 206 | result.push({ 207 | name: '被蕴含的定义', 208 | result: new ImplyNode(node.ch(1), node.ch(0)), 209 | }); 210 | break; 211 | case 'imply': 212 | if (node.ch(1).type === 'imply') { 213 | result.push({ 214 | name: '分配律', 215 | result: new ImplyNode( 216 | new ImplyNode(node.ch(0), node.ch(1).ch(0)), 217 | new ImplyNode(node.ch(0), node.ch(1).ch(1)), 218 | ), 219 | }); 220 | } 221 | if (node.ch(0).toString() === node.ch(1).toString()) { 222 | result.push({ 223 | name: '等幂律', 224 | result: new TrueNode(), 225 | }); 226 | } 227 | if (node.ch(0).type === 'true') { 228 | result.push({ 229 | name: '同一律', 230 | result: node.ch(1), 231 | }); 232 | } 233 | if (node.ch(1).type === 'false') { 234 | result.push({ 235 | name: '同一律', 236 | result: new NotNode(node.ch(0)), 237 | }); 238 | } 239 | if (node.ch(1).type === 'true') { 240 | result.push({ 241 | name: '零律', 242 | result: node.ch(1), 243 | }); 244 | } 245 | if (node.ch(0).type === 'false') { 246 | result.push({ 247 | name: '零律', 248 | result: new TrueNode(), 249 | }); 250 | } 251 | if (node.ch(1).type === 'not' && node.ch(0).toString() === node.ch(1).ch(0).toString()) { 252 | result.push({ 253 | name: '补余律', 254 | result: node.ch(1), 255 | }); 256 | } 257 | if (node.ch(0).type === 'not' && node.ch(1).toString() === node.ch(0).ch(0).toString()) { 258 | result.push({ 259 | name: '补余律', 260 | result: node.ch(1), 261 | }); 262 | } 263 | result.push({ 264 | name: '蕴含等值式', 265 | result: new OrNode(new NotNode(node.ch(0)), node.ch(1)), 266 | }, { 267 | name: '假言易位', 268 | result: new ImplyNode(new NotNode(node.ch(1)), new NotNode(node.ch(0))), 269 | }); 270 | if (node.ch(0).type === 'not' && node.ch(1).type === 'not') { 271 | result.push({ 272 | name: '假言易位', 273 | result: new ImplyNode(node.ch(1).ch(0), node.ch(0).ch(0)), 274 | }); 275 | } 276 | if (node.ch(1).type === 'imply') { 277 | result.push({ 278 | name: '前提合取合并', 279 | result: new ImplyNode(new AndNode(node.ch(0), node.ch(1).ch(0)), node.ch(1).ch(1)), 280 | }, { 281 | name: '前提交换', 282 | result: new ImplyNode(node.ch(1).ch(0), new ImplyNode(node.ch(0), node.ch(1).ch(1))), 283 | }); 284 | } 285 | if (node.ch(0).type === 'and') { 286 | result.push({ 287 | name: '前提合取合并', 288 | result: new ImplyNode(node.ch(0).ch(0), new ImplyNode(node.ch(0).ch(1), node.ch(1))), 289 | }); 290 | } 291 | if (node.ch(0).type === 'or') { 292 | result.push({ 293 | name: '前提析取合并', 294 | result: new AndNode( 295 | new ImplyNode(node.ch(0).ch(0), node.ch(1)), 296 | new ImplyNode(node.ch(0).ch(1), node.ch(1)), 297 | ), 298 | }); 299 | } 300 | break; 301 | case 'nand': 302 | result.push({ 303 | name: '与非的定义', 304 | result: new NotNode(new AndNode(node.ch(0), node.ch(1))), 305 | }); 306 | break; 307 | case 'nor': 308 | result.push({ 309 | name: '或非的定义', 310 | result: new NotNode(new OrNode(node.ch(0), node.ch(1))), 311 | }); 312 | break; 313 | case 'not': 314 | if (node.ch(0).type === 'not') { 315 | result.push({ 316 | name: '双重否定律', 317 | result: node.ch(0).ch(0), 318 | }); 319 | } 320 | if (node.ch(0).type === 'or') { 321 | result.push({ 322 | name: '摩根律', 323 | result: new AndNode(new NotNode(node.ch(0).ch(0)), new NotNode(node.ch(0).ch(1))), 324 | }); 325 | } 326 | if (node.ch(0).type === 'and') { 327 | result.push({ 328 | name: '摩根律', 329 | result: new OrNode(new NotNode(node.ch(0).ch(0)), new NotNode(node.ch(0).ch(1))), 330 | }); 331 | } 332 | if (node.ch(0).type === 'imply') { 333 | result.push({ 334 | name: '摩根律', 335 | result: new AndNode(node.ch(0).ch(0), new NotNode(node.ch(0).ch(1))), 336 | }); 337 | } 338 | if (node.ch(0).type === 'eq') { 339 | result.push({ 340 | name: '摩根律', 341 | result: new OrNode( 342 | new AndNode(new NotNode(node.ch(0).ch(0)), node.ch(0).ch(1)), 343 | new AndNode(node.ch(0).ch(0), new NotNode(node.ch(0).ch(1))), 344 | ), 345 | }); 346 | } 347 | if (node.ch(0).type === 'true') { 348 | result.push({ 349 | name: '¬T = F', 350 | result: new FalseNode(), 351 | }); 352 | } 353 | if (node.ch(0).type === 'false') { 354 | result.push({ 355 | name: '¬F = T', 356 | result: new TrueNode(), 357 | }); 358 | } 359 | break; 360 | case 'or': 361 | if (node.ch(0).type === node.type) { 362 | result.push({ 363 | name: '结合律', 364 | result: new OrNode(node.ch(0).ch(0), new OrNode(node.ch(0).ch(1), node.ch(1))), 365 | }); 366 | } 367 | if (node.ch(1).type === node.type) { 368 | result.push({ 369 | name: '结合律', 370 | result: new OrNode(new OrNode(node.ch(0), node.ch(1).ch(0)), node.ch(1).ch(1)), 371 | }); 372 | } 373 | result.push({ 374 | name: '交换律', 375 | result: new OrNode(node.ch(1), node.ch(0)), 376 | }); 377 | if (node.ch(1).type === 'and') { 378 | result.push({ 379 | name: '分配律', 380 | result: new AndNode( 381 | new OrNode(node.ch(0), node.ch(1).ch(0)), 382 | new OrNode(node.ch(0), node.ch(1).ch(1)), 383 | ), 384 | }); 385 | } 386 | if (node.ch(0).type === 'and' && node.ch(1).type === 'and' && node.ch(0).ch(0).toString() === node.ch(1).ch(0).toString()) { 387 | result.push({ 388 | name: '分配律', 389 | result: new AndNode(node.ch(0).ch(0), new OrNode(node.ch(0).ch(1), node.ch(1).ch(1))), 390 | }); 391 | } 392 | if (node.ch(0).toString() === node.ch(1).toString()) { 393 | result.push({ 394 | name: '等幂律', 395 | result: node.ch(0), 396 | }); 397 | } 398 | if (node.ch(1).type === 'and' && node.ch(0).toString() === node.ch(1).ch(0).toString()) { 399 | result.push({ 400 | name: '吸收律', 401 | result: node.ch(0), 402 | }); 403 | } 404 | if (node.ch(0).type === 'not' && node.ch(1).type === 'not') { 405 | result.push({ 406 | name: '摩根律', 407 | result: new NotNode(new AndNode(node.ch(0).ch(0), node.ch(1).ch(0))), 408 | }); 409 | } 410 | if (node.ch(0).type === 'and' && node.ch(1).type === 'and' 411 | && node.ch(0).ch(0).type === 'not' && node.ch(1).ch(1).type === 'not' 412 | && node.ch(0).ch(0).ch(0).toString() === node.ch(1).ch(0).toString() 413 | && node.ch(0).ch(1).toString() === node.ch(1).ch(1).ch(0).toString()) { 414 | result.push({ 415 | name: '摩根律', 416 | result: new NotNode(new EqNode(node.ch(1).ch(0), node.ch(0).ch(1))), 417 | }); 418 | } 419 | if (node.ch(1).type === 'false') { 420 | result.push({ 421 | name: '同一律', 422 | result: node.ch(0), 423 | }); 424 | } 425 | if (node.ch(1).type === 'true') { 426 | result.push({ 427 | name: '零律', 428 | result: node.ch(1), 429 | }); 430 | } 431 | if (node.ch(1).type === 'not' && node.ch(0).toString() === node.ch(1).ch(0).toString()) { 432 | result.push({ 433 | name: '补余律', 434 | result: new TrueNode(), 435 | }); 436 | } 437 | if (node.ch(0).type === 'not') { 438 | result.push({ 439 | name: '蕴涵等值式', 440 | result: new ImplyNode(node.ch(0).ch(0), node.ch(1)), 441 | }); 442 | } 443 | if (node.ch(0).type === 'and' && node.ch(1).type === 'and' 444 | && node.ch(1).ch(0).type === 'not' && node.ch(1).ch(1).type === 'not' 445 | && node.ch(0).ch(0).toString() === node.ch(1).ch(0).ch(0).toString() 446 | && node.ch(0).ch(1).toString() === node.ch(1).ch(1).ch(0).toString()) { 447 | result.push({ 448 | name: '从取真来描述双条件', 449 | result: new EqNode(node.ch(0).ch(0), node.ch(0).ch(1)), 450 | }); 451 | } 452 | break; 453 | case 'xor': 454 | result.push({ 455 | name: '异或的定义', 456 | result: new NotNode(new EqNode(node.ch(0), node.ch(1))), 457 | }); 458 | break; 459 | case 'true': 460 | result.push({ 461 | name: 'T = ¬F', 462 | result: new NotNode(new FalseNode()), 463 | }); 464 | break; 465 | default: 466 | exhaustiveCheck(node.type); 467 | } 468 | 469 | return result; 470 | } 471 | -------------------------------------------------------------------------------- /src/core/getTable.ts: -------------------------------------------------------------------------------- 1 | import { h, RenderFunction } from 'vue'; 2 | import { AstNode, AtomNode } from './AstNode'; 3 | import Data from '~/types/data'; 4 | import Step from '~/types/step'; 5 | 6 | import SimplificationButton from '~/components/SimplificationButton.vue'; 7 | 8 | interface Column { 9 | title: string | RenderFunction; 10 | key: string; 11 | align: 'center'; 12 | children?: Column[]; 13 | className?: string; 14 | } 15 | 16 | export type { Column }; 17 | 18 | function getColumn(u: AstNode, root: AstNode, addStep: (step: Step) => void): Column { 19 | const { children, type } = u; 20 | 21 | const result: Column = { 22 | title: type !== 'atom' ? () => h(SimplificationButton, { 23 | node: u, 24 | root, 25 | addStep, 26 | }) : u.toString(), 27 | key: u.toString(), 28 | align: 'center', 29 | }; 30 | 31 | const main = { 32 | title: u.operator, 33 | key: u.toString(), 34 | align: 'center', 35 | ...(u === root ? { 36 | className: 'truth-table-result', 37 | } : {}), 38 | } as const; 39 | 40 | if (children.length === 1) { 41 | result.children = [ 42 | main, 43 | getColumn(children[0], root, addStep), 44 | ]; 45 | } else if (children.length === 2) { 46 | result.children = [ 47 | getColumn(children[0], root, addStep), 48 | main, 49 | getColumn(children[1], root, addStep), 50 | ]; 51 | } 52 | 53 | return result; 54 | } 55 | 56 | export function getTable( 57 | root: AstNode, 58 | atomNodes: Map, 59 | addStep: (step: Step) => void, 60 | ) { 61 | const columns: Column[] = []; 62 | 63 | const atoms = Array.from(atomNodes.keys()).sort(); 64 | atoms.forEach((atom) => { 65 | columns.push({ 66 | title: atom, 67 | key: atom, 68 | align: 'center', 69 | }); 70 | }); 71 | 72 | columns.push(getColumn(root, root, addStep)); 73 | 74 | const datas: Data[] = []; 75 | const truths: boolean[] = []; 76 | 77 | for (let mask = 0; mask < (1 << atoms.length); mask += 1) { 78 | const data: Data = { key: mask }; 79 | for (let i = 0; i < atoms.length; i += 1) { 80 | data[atoms[i]] = (mask >> (atoms.length - 1 - i)) & 1; 81 | } 82 | 83 | truths.push(root.dfsTruth(data)); 84 | datas.push(data); 85 | } 86 | 87 | return { 88 | atoms, 89 | columns, 90 | data: datas, 91 | truths, 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/core/transformExp.spec.ts: -------------------------------------------------------------------------------- 1 | import transformExp from './transformExp'; 2 | 3 | describe('transformExp', () => { 4 | it('should transform brackets', () => { 5 | const result = transformExp('(([【{{))]】}}()【】}{[])({}'); 6 | expect(result).toBe('(((((())))))()())(())(()'); 7 | }); 8 | 9 | it('should transform NOT', () => { 10 | const result = transformExp('!!¬!NoT~~~~¬nOt~~'); 11 | expect(result).toBe('!!!!!!!!!!!!!'); 12 | }); 13 | 14 | it('should transform AND', () => { 15 | const result = transformExp('∧∧AnD&∧&&∧&&&&aND&'); 16 | expect(result).toBe('&&&&&&&&&&&'); 17 | }); 18 | 19 | it('should transform NAND', () => { 20 | const result = transformExp('⊼↑NAND↑⊼⊼'); 21 | expect(result).toBe('↑↑↑↑↑↑'); 22 | }); 23 | 24 | it('should distinguish NAND with AND', () => { 25 | const result = transformExp('AND nAND NAnD aND NANDAND ANDNAND'); 26 | expect(result).toBe('&↑↑&NANDANDANDNAND'); 27 | }); 28 | 29 | it('should transform XOR', () => { 30 | const result = transformExp('⊕!=!=^⊻XoR…………!=^……⊻xOR'); 31 | expect(result).toBe('^^^^^^^^^^^^^'); 32 | }); 33 | 34 | it('should transform OR', () => { 35 | const result = transformExp('oR∨||||||oROr||OR'); 36 | expect(result).toBe('||||||oROr||'); 37 | }); 38 | 39 | it('should transform NOR', () => { 40 | const result = transformExp('↓NOr⊽↓⊽↓nor↓'); 41 | expect(result).toBe('↓↓↓↓↓↓↓↓'); 42 | }); 43 | 44 | it('should distinguish OR, XOR and NOR', () => { 45 | const result = transformExp('or nor xor XNOR NXOR x oRn OR'); 46 | expect(result).toBe('|↓^XNORNXORxoRn|'); 47 | }); 48 | 49 | it('should transform IMPLIES', () => { 50 | const result = transformExp('》>->→→-->implIes》IMPliES'); 51 | expect(result).toBe('>>>>>->>>>'); 52 | }); 53 | 54 | it('should transform IMPLIEDBY', () => { 55 | const result = transformExp('《<<-←←<--implIedby《IMPlieDBY'); 56 | expect(result).toBe('<<<<<<-<<<'); 57 | }); 58 | 59 | it('should transform EQ', () => { 60 | const result = transformExp('⟷<>=<->eq==⟷<->====↔Eq'); 61 | expect(result).toBe('============'); 62 | }); 63 | 64 | it('should distinguish <> <- ->', () => { 65 | const result = transformExp('<< <-> >> <> --> <<--'); 66 | expect(result).toBe('<<=>>=-><<-'); 67 | }); 68 | 69 | it('should have \\b', () => { 70 | const origin = 'NotAndNandXorOrNorImpliesImpliedbyEq'; 71 | const result = transformExp(origin); 72 | expect(result).toBe(origin); 73 | }); 74 | 75 | it('should remove spaces', () => { 76 | const result = transformExp(' \n\t\rAND\r\t\n\v '); 77 | expect(result).toBe('&'); 78 | }); 79 | 80 | it('should keep capital letters', () => { 81 | const result = transformExp('Q W E R T Y U I O P A S D F G H J K L Z X C V B N M'); 82 | expect(result).toBe('QWERTYUIOPASDFGHJKLZXCVBNM'); 83 | }); 84 | 85 | it('should transform LaTeX', () => { 86 | const result = transformExp('\\uparrow\\downarrow\\land\\lor\\xor\\nand\\neg\\lor\\leftrightarrow\\lnot\\vee\\wedge\\gets\\to\\land\\nand\\nor\\rightarrow\\leftarrow\\land\\lor\\xor\\nand\\neg\\lor\\leftrightarrow\\gets\\to\\land\\nand\\nor\\rightarrow\\leftarrow'); 87 | expect(result).toBe('↑↓&|^↑!|=!|&<>&↑↓><&|^↑!|=<>&↑↓><'); 88 | }); 89 | 90 | it('should have \\b for LaTeX', () => { 91 | const origin = '\\uparrowx\\downarrowx\\landx\\lorx\\xorx\\nandx\\negx\\lorx\\leftrightarrowx\\lnotx\\veex\\wedgex\\getsx\\tox\\landx\\nandd\\norr\\rightarroww\\leftarroww'; 92 | const result = transformExp(origin); 93 | expect(result).toBe(origin); 94 | }); 95 | 96 | it('should require "\\" for LaTeX', () => { 97 | const result = transformExp('uparrow downarrow land lor xor nand neg lor leftrightarrow lnot vee wedge gets to land nand nor rightarrow leftarrow'); 98 | expect(result).toBe('uparrowdownarrowlandlor^↑neglorleftrightarrowlnotveewedgegetstoland↑↓rightarrowleftarrow'); 99 | }); 100 | 101 | it('should require lowercase for LaTeX', () => { 102 | const result = transformExp('\\Uparrow\\Downarrow\\Land\\Lor\\Xor\\Nand\\Neg\\Lor\\Leftrightarrow\\Lnot\\Vee\\Wedge\\Gets\\To\\Land\\Nand\\Nor\\Rightarrow\\Leftarrow'); 103 | expect(result).toBe('\\Uparrow\\Downarrow\\Land\\Lor\\^\\↑\\Neg\\Lor\\Leftrightarrow\\Lnot\\Vee\\Wedge\\Gets\\To\\Land\\↑\\↓\\Rightarrow\\Leftarrow'); 104 | }); 105 | 106 | it('should accept true/false as T/F', () => { 107 | const result = transformExp('true false True fALsE truefalse'); 108 | expect(result).toBe('TFTFtruefalse'); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/core/transformExp.ts: -------------------------------------------------------------------------------- 1 | export default function transformExp(exp: string): string { 2 | return exp 3 | .replace(/\\(land|wedge)\b/g, '&') 4 | .replace(/∧|&&|\bAND\b/gi, '&') 5 | .replace(/\\(nand|uparrow)\b/g, '↑') 6 | .replace(/⊼|\bNAND\b/gi, '↑') 7 | .replace(/\\xor\b/g, '^') 8 | .replace(/⊻|⊕|!=|……|\bXOR\b/gi, '^') 9 | .replace(/\\(nor|downarrow)\b/g, '↓') 10 | .replace(/⊽|\bNOR\b/gi, '↓') 11 | .replace(/\\(lor|vee)\b/g, '|') 12 | .replace(/∨|||\|\||\bOR\b/gi, '|') 13 | .replace(/\\leftrightarrow\b/g, '=') 14 | .replace(/⟷|↔|==|<->|<>|\bEQ\b/gi, '=') 15 | .replace(/\\(to|rightarrow)\b/g, '>') 16 | .replace(/→|》|->|\bIMPLIES\b/gi, '>') 17 | .replace(/\\(gets|leftarrow)\b/g, '<') 18 | .replace(/←|《|<-|\bIMPLIEDBY\b/gi, '<') 19 | .replace(/\\(neg|lnot)\b/g, '!') 20 | .replace(/¬|~|!|~|\bNOT\b/gi, '!') 21 | .replace(/(|\[|【|\{|{/g, '(') 22 | .replace(/)|\]|】|\}|}/g, ')') 23 | .replace(/\btrue\b/gi, 'T') 24 | .replace(/\bfalse\b/gi, 'F') 25 | .replace(/\s/g, ''); 26 | } 27 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from '~/App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | 4 | const component: DefineComponent, Record, unknown>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/WangHaoTooLongError.ts: -------------------------------------------------------------------------------- 1 | export default class WangHaoTooLongError extends Error { } 2 | -------------------------------------------------------------------------------- /src/types/data.ts: -------------------------------------------------------------------------------- 1 | export default interface Data { 2 | [index: string]: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/equivalent.ts: -------------------------------------------------------------------------------- 1 | import type AstNode from '~/core/AstNode/base'; 2 | 3 | export default interface Equivalent { 4 | name: string; 5 | result: AstNode; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/step.ts: -------------------------------------------------------------------------------- 1 | export default interface Step { 2 | exp: string; 3 | rule: string; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "es2015", 5 | "strict": true, 6 | "importHelpers": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "node", 18 | "vite/client", 19 | "jest" 20 | ], 21 | "paths": { 22 | "~/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "es2015", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.vue" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import { resolve } from 'path'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | vue(), 8 | ], 9 | resolve: { 10 | alias: { 11 | '~': resolve(__dirname, 'src'), 12 | }, 13 | }, 14 | base: '', 15 | build: { 16 | target: 'es2015', 17 | minify: 'terser', 18 | }, 19 | }); 20 | --------------------------------------------------------------------------------