diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf6ca8..8bfd46f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version: Exet v0.90, December 5, 2023 + +- In the "Analysis" panel, also show a histogram of duplicated substrings + of length 3 or longer in solution entries. + ### Version: Exet v0.89, November 25, 2023 - Make the RHS of the UI responsive, adapting to the available height. diff --git a/README.md b/README.md index 17ff0ff..88f23ae 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## A web app for crossword construction -#### Version: Exet v0.89 November 25, 2023 +#### Version: Exet v0.90, December 5, 2023 #### Author: Viresh Ratnakar @@ -741,6 +741,7 @@ properties of crosswords: very short or very long. - Words (other than very common ones) should not be repeated in clues, especially if they are used as cryptic wordplay indicators. +- Long common substrings in solution entries may not be desirable. - The number of long and very long clues should ideally be limited. - Wordplay types in cryptic clues should have a good mix of variety. - Use of uncommon solution entries should be limited. Clues for @@ -796,6 +797,11 @@ Here is an illustrative example of the kinds of analyses shown: > - 2 × wild > ... > Distinct values: 23 +> - Substrings repeated in solution entries: +> - 4 × INE +> - 2 × FIELD +> ... +> Distinct values: 7 > - Word-lengths of set clues: > - 1 count of 4 > - 6 counts of 5 diff --git a/about-exet.html b/about-exet.html index cdff163..e6fb03a 100644 --- a/about-exet.html +++ b/about-exet.html @@ -24,7 +24,7 @@

Exet

A web app for crossword construction

-

Version: Exet v0.89, November 25, 2023

+

Version: Exet v0.90, December 5, 2023

Software by Viresh Ratnakar

- + - - - + + + Exet Brazilian: Create a crossword in Portuguese diff --git a/exet-hindi.html b/exet-hindi.html index ac9bf76..59c9913 100644 --- a/exet-hindi.html +++ b/exet-hindi.html @@ -10,11 +10,11 @@ See the full license notice in exet.js. -Current version: v0.89, November 25, 2023 +Current version: v0.90, December 5, 2023 --> - + - - - + + + Exet Hindi: Create a crossword in Hindi diff --git a/exet-lexicon.js b/exet-lexicon.js index 89fd59c..146c7ef 100644 --- a/exet-lexicon.js +++ b/exet-lexicon.js @@ -5,7 +5,7 @@ Copyright (c) 2022 Viresh Ratnakar See the full Exet license notice in exet.js. -Current version: v0.89 November 25, 2023 +Current version: v0.90, December 5, 2023 */ /** diff --git a/exet-version.txt b/exet-version.txt index b2ba4dd..a705e1a 100644 --- a/exet-version.txt +++ b/exet-version.txt @@ -1 +1 @@ -v0.89 +v0.90 diff --git a/exet.html b/exet.html index 4cda4ea..e1be757 100644 --- a/exet.html +++ b/exet.html @@ -10,10 +10,10 @@ See the full license notice in exet.js. -Current version: v0.89, November 25, 2023 +Current version: v0.90, December 5, 2023 --> - + - - - + + + Exet: Create a crossword diff --git a/exet.js b/exet.js index 4badc10..4e60b79 100644 --- a/exet.js +++ b/exet.js @@ -24,7 +24,7 @@ SOFTWARE. The latest code and documentation for Exet can be found at: https://github.com/viresh-ratnakar/exet -Current version: v0.89, November 25, 2023 +Current version: v0.90, December 5, 2023 */ function ExetModals() { @@ -663,7 +663,7 @@ function ExetRev(id, title, revNum, revType, timestamp, details="") { }; function Exet() { - this.version = 'v0.89, November 25, 2023'; + this.version = 'v0.90, December 5, 2023'; this.puz = null; this.prefix = ''; this.suffix = ''; @@ -2206,6 +2206,7 @@ function ExetLightInfo() { this.filled = 0 this.lengths = {} this.clueLengths = {} + this.substrings = {} this.popularities = {} this.letters = {} for (let c of exetLexicon.letters) { @@ -2218,100 +2219,152 @@ function ExetLightInfo() { this.annotations = {} } +Exet.prototype.getSubstrings = function(s) { + const letters = exetLexicon.letterString(s); + const substrings = new Set; + for (let len = 3; len <= letters.length; len++) { + const end = letters.length - len; + for (let start = 0; start <= end; start++) { + substrings.add(letters.substr(start, len)); + } + } + return substrings; +} + +/** + * Remove "subsumed" substrings from the histogram. + */ +Exet.prototype.trimSubsumedSubstrings = function(substrings) { + const toDelete = []; + const keys = Object.keys(substrings); + for (const sub of keys) { + let subsumed = false; + for (const sup of keys) { + if (sup.length <= sub.length || + substrings[sub].count != substrings[sup].count || + sup.indexOf(sub) < 0) { + continue; + } + subsumed = true; + break; + } + if (subsumed) { + toDelete.push(sub); + continue; + } + } + for (const sub of toDelete) { + delete substrings[sub]; + } +} + Exet.prototype.getLightInfos = function() { - let infos = { + const infos = { All: new ExetLightInfo(), Across: new ExetLightInfo(), Down: new ExetLightInfo(), Other: new ExetLightInfo(), - } - let allInfo = infos['All'] - let aInfo = infos['Across'] - let dInfo = infos['Down'] - let oInfo = infos['Other'] - for (let ci in this.puz.clues) { - let theClue = this.puz.clues[ci] - let dirInfo = theClue.dir == 'A' ? aInfo : (theClue.dir == 'D' ? - dInfo : oInfo) - allInfo.lights++ - dirInfo.lights++ + }; + const allInfo = infos['All']; + const aInfo = infos['Across']; + const dInfo = infos['Down']; + const oInfo = infos['Other']; + for (const ci in this.puz.clues) { + const theClue = this.puz.clues[ci]; + const dirInfo = theClue.dir == 'A' ? + aInfo : (theClue.dir == 'D' ? dInfo : oInfo); + allInfo.lights++; + dirInfo.lights++; if (theClue.parentClueIndex) { - allInfo.ischild++ - dirInfo.ischild++ - continue + allInfo.ischild++; + dirInfo.ischild++; + continue; } - let label = theClue.label + theClue.dir.toLowerCase() + let label = theClue.label + theClue.dir.toLowerCase(); if (theClue.solution && theClue.solution.indexOf('?') < 0) { - allInfo.filled += 1 - dirInfo.filled += 1 - let lexl = exetLexicon.lexicon.length - let index = lexl - let fillClue = this.fillState.clues[ci] + allInfo.filled += 1; + dirInfo.filled += 1; + const lexl = exetLexicon.lexicon.length; + let index = lexl; let solText = theClue.solution; + const fillClue = this.fillState.clues[ci]; if (fillClue && fillClue.lChoices.length == 1) { - index = fillClue.lChoices[0] + index = fillClue.lChoices[0]; solText = exetLexicon.getLex(index); } - let pop = 5 * Math.round(20 * (lexl - index) / lexl) + let pop = 5 * Math.round(20 * (lexl - index) / lexl); label += ': ' + solText; - this.addStat(allInfo.popularities, pop, label) - this.addStat(dirInfo.popularities, pop, label) + this.addStat(allInfo.popularities, pop, label); + this.addStat(dirInfo.popularities, pop, label); + const substrings = this.getSubstrings(solText); + for (const substring of substrings) { + this.addStat(allInfo.substrings, substring, label); + this.addStat(dirInfo.substrings, substring, label); + } } - this.addStat(allInfo.lengths, theClue.enumLen, label) - this.addStat(dirInfo.lengths, theClue.enumLen, label) - let depunctClue = exetLexicon.depunct(theClue.clue) + this.addStat(allInfo.lengths, theClue.enumLen, label); + this.addStat(dirInfo.lengths, theClue.enumLen, label); + let depunctClue = exetLexicon.depunct(theClue.clue); if (depunctClue && !this.isDraftClue(theClue.clue)) { - allInfo.set += 1 - dirInfo.set += 1 - let words = depunctClue.split(' ') + allInfo.set += 1; + dirInfo.set += 1; + let words = depunctClue.split(' '); for (let word of words) { - this.addStat(allInfo.words, word, label) - this.addStat(dirInfo.words, word, label) + this.addStat(allInfo.words, word, label); + this.addStat(dirInfo.words, word, label); } - this.addStat(allInfo.clueLengths, words.length, label) - this.addStat(dirInfo.clueLengths, words.length, label) + this.addStat(allInfo.clueLengths, words.length, label); + this.addStat(dirInfo.clueLengths, words.length, label); } if (theClue.anno) { - allInfo.annos += 1 - dirInfo.annos += 1 - let anno = this.essenceOfAnno(theClue.anno) + allInfo.annos += 1; + dirInfo.annos += 1; + let anno = this.essenceOfAnno(theClue.anno); if (anno) { - this.addStat(allInfo.annotations, anno, label) - this.addStat(dirInfo.annotations, anno, label) + this.addStat(allInfo.annotations, anno, label); + this.addStat(dirInfo.annotations, anno, label); } } } - // In *.words, retain only those that have count > 1 - for (let key of Object.keys(infos)) { - let info = infos[key] - for (let word of Object.keys(info.words)) { - if (info.words[word].count <= 1) delete info.words[word] + // In *.words and *.substrings, retain only those that have count > 1 + for (const key of Object.keys(infos)) { + const info = infos[key] + for (const word of Object.keys(info.words)) { + if (info.words[word].count <= 1) { + delete info.words[word]; + } + } + for (const substring of Object.keys(info.substrings)) { + if (info.substrings[substring].count <= 1) { + delete info.substrings[substring]; + } } + this.trimSubsumedSubstrings(info.substrings); } - let grid = this.puz.grid - let w = this.puz.gridWidth - let h = this.puz.gridHeight + const grid = this.puz.grid; + const w = this.puz.gridWidth; + const h = this.puz.gridHeight; for (let i = 0; i < h; i++) { for (let j = 0; j < w; j++) { - let gridCell = grid[i][j] - if (!gridCell.isLight || gridCell.solution == '?') continue - let rowcol = 'row-' + (h - i) + ',' + 'col-' + (j + 1) - this.addStat(allInfo.letters, gridCell.solution, rowcol) + const gridCell = grid[i][j]; + if (!gridCell.isLight || gridCell.solution == '?') continue; + const rowcol = 'row-' + (h - i) + ',' + 'col-' + (j + 1); + this.addStat(allInfo.letters, gridCell.solution, rowcol); if (gridCell.acrossClueLabel) { - this.addStat(aInfo.letters, gridCell.solution, rowcol) + this.addStat(aInfo.letters, gridCell.solution, rowcol); } if (gridCell.downClueLabel) { - this.addStat(dInfo.letters, gridCell.solution, rowcol) + this.addStat(dInfo.letters, gridCell.solution, rowcol); } if (gridCell.z3dClueLabel) { - this.addStat(dInfo.letters, gridCell.solution, rowcol) + this.addStat(dInfo.letters, gridCell.solution, rowcol); } } } if (oInfo.lights == 0) { - delete infos['Other'] + delete infos['Other']; } - return infos + return infos; } Exet.prototype.updateAnalysis = function(elt) { @@ -2389,8 +2442,10 @@ Exet.prototype.updateAnalysis = function(elt) { this.plotStats(info.popularities, 5)}`; html += `Letters used:
${this.plotStats(info.letters)}`; - html += `Words repeated in set clues:
${this.plotStats( - info.words)}`; + html += `
Words repeated in set clues:
${this.plotStats( + info.words)}`; + html += `
Substrings repeated in solution entries:
${this.plotStats( + info.substrings)}
`; html += `
Word-lengths of set clues:
${this.plotStats( info.clueLengths)}
`; html += `
Annotations provided in clues: ${info.annos} (${(