nmx.sudokugen.library 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![Sudoku Gen](https://raw.githubusercontent.com/newmediaxp/sudokugen/main/icon.png)
2
+
1
3
  # Sudoku Gen Library
2
4
 
3
5
  `sudoku` `javascript` `typescript` `library` `npm`
@@ -99,8 +101,31 @@ A square that is rule-clean but still wrong (`board[i] !== solution[i]`) returns
99
101
 
100
102
  | Function | Signature | Description |
101
103
  |-----------------|---------------------------------------------------|---------------------------------------------------------------------------------------------------------|
102
- | `getPuzzleCode` | `getPuzzleCode(grid: ArrayLike<number>): string` | Encodes a grid as a puzzle code: one char per square (`0` blank, `1`-`9`, `A` = 10…), `.` between rows. |
103
- | `getPuzzleArr` | `getPuzzleArr(code: string): number[]` | Parses a puzzle code back into a grid array; unrecognised characters are ignored. |
104
+ | `getPuzzleCode` | `getPuzzleCode(grid: ArrayLike<number>, encoding?: EncodingOption): string` | Encodes a grid as a puzzle code: one char per square (`0` blank, `1`-`9`, `A` = 10…), `.` between rows. With an `encoding`, writes in that one instead (default `All`). Throws a `RangeError` if a value doesn't fit the encoding. |
105
+ | `getPuzzleArr` | `getPuzzleArr(code: string, encoding?: EncodingOption): number[]` | Parses a puzzle code back into a grid array; unrecognised characters are ignored. The encoding can't be detected from the code, so pass the one it was written in (default `All`). |
106
+ | `getSymbol` | `getSymbol(value: number, encoding: EncodingOption): string` | The character one square value is written as. |
107
+ | `maxRank` | `maxRank(encoding: EncodingOption): number` | The highest rank the encoding can write. |
108
+
109
+ ### `EncodingOption`
110
+
111
+ Frozen character sets for puzzle codes. Blank is `0` in every one. Letters read in either case.
112
+
113
+ | Value | Values written as | Max rank |
114
+ |---------------------|------------------------------------------------|----------|
115
+ | `All` | `1`-`9`, then `A`-`Z` (`A` = 10). The default. | 5 |
116
+ | `AllMinusO` | `1`-`9`, then `A`-`Z` without `O` | 5 |
117
+ | `DigitsOnly` | `1`-`9` | 3 |
118
+ | `LettersOnly` | `A`-`Z` (`A` = 1) | 5 |
119
+ | `LettersOnlyMinusO` | `A`-`Z` without `O` (`A` = 1) | 5 |
120
+
121
+ The minus-O sets skip `O` so it can't be misread as the blank `0`.
122
+
123
+ ```js
124
+ import { EncodingOption, getPuzzleArr, getPuzzleCode } from "nmx.sudokugen.library";
125
+
126
+ const a_letters = getPuzzleCode(a_sudoku.puzzle, EncodingOption.LettersOnly); // e.g. "CI0EF00BG.00AH0I0DE...."
127
+ const a_grid = getPuzzleArr(a_letters, EncodingOption.LettersOnly); // read it back in the same encoding
128
+ ```
104
129
 
105
130
  ### Notes
106
131
 
@@ -132,6 +157,10 @@ A square that is rule-clean but still wrong (`board[i] !== solution[i]`) returns
132
157
 
133
158
  * None. Needs an ES2022 runtime: current browsers, Node.js 20 or later, Deno, Bun.
134
159
 
160
+ * Used in
161
+
162
+ * Sudoku Gen web app [(link)](https://www.newmediaxp.com/app/sudokugen)
163
+
135
164
  * Other languages
136
165
 
137
166
  * C# library: [NMX.SudokuGen.Library](https://www.nuget.org/packages/NMX.SudokuGen.Library)
@@ -142,7 +171,7 @@ A square that is rule-clean but still wrong (`board[i] !== solution[i]`) returns
142
171
 
143
172
  * Source
144
173
 
145
- * [GitHub](https://github.com/newmediaxp/sudokugen/tree/main/js/nmx.sudokugen.library)
174
+ * [GitHub](https://github.com/newmediaxp/sudokugen/tree/main/javascript/nmx.sudokugen.library)
146
175
 
147
176
  ---
148
177
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { Conflict, Sudoku } from "./sudoku.ts";
2
2
  export type { CreateOptions } from "./sudoku.ts";
3
- export { getPuzzleArr, getPuzzleCode } from "./utility.ts";
3
+ export { EncodingOption, getPuzzleArr, getPuzzleCode, getSymbol, maxRank } from "./utility.ts";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Copyright (c) 2024-2026 New Media XP. Licensed under the MIT License.
2
2
  export { Conflict, Sudoku } from "./sudoku.js";
3
- export { getPuzzleArr, getPuzzleCode } from "./utility.js";
3
+ export { EncodingOption, getPuzzleArr, getPuzzleCode, getSymbol, maxRank } from "./utility.js";
package/dist/utility.d.ts CHANGED
@@ -1,19 +1,61 @@
1
1
  import type { Random } from "./random.ts";
2
2
  /**
3
- * Encodes a grid as one character per square, row by row: 0 = blank, 1-9 as digits,
4
- * 10 and above as letters ('A' = 10), '.' between rows.
5
- * Values above 35 ('Z') are unrepresentable, so rank 5 (25x25) at most.
3
+ * Character sets a puzzle code can be written in. Blank (0) is '0' in every one of them.
4
+ */
5
+ export declare const EncodingOption: Readonly<{
6
+ /** 1 to 9 as digits, then 10 to 35 as A to Z. Fits every rank. */
7
+ readonly All: 0;
8
+ /** 1 to 9 as digits, then 10 to 34 as A to Z without O, so it can't be misread as blank '0'. Fits every rank. */
9
+ readonly AllMinusO: 1;
10
+ /** 1 to 9 as digits only. Fits rank 3 and below. */
11
+ readonly DigitsOnly: 2;
12
+ /** 1 to 26 as A to Z. Fits every rank. */
13
+ readonly LettersOnly: 3;
14
+ /** 1 to 25 as A to Z without O, so it can't be misread as blank '0'. Fits every rank. */
15
+ readonly LettersOnlyMinusO: 4;
16
+ }>;
17
+ /** One of the {@link EncodingOption} values. */
18
+ export type EncodingOption = (typeof EncodingOption)[keyof typeof EncodingOption];
19
+ /**
20
+ * The highest rank whose grids an encoding can write; a rank r grid holds values up to r^2.
21
+ * Never above Sudoku.maxRank, since no encoding writes past 35.
22
+ * @param p_encoding The character set to check.
23
+ * @returns 3 for {@link EncodingOption.DigitsOnly}, 5 for every other encoding.
24
+ * @throws {RangeError} The encoding is not an {@link EncodingOption} value.
25
+ */
26
+ export declare function maxRank(p_encoding: EncodingOption): number;
27
+ /**
28
+ * The character an encoding writes for one square value; blank (0) is '0' in every encoding.
29
+ * @param p_value The square value, from 0 (blank) up to what the encoding can write.
30
+ * @param p_encoding The character set to write it in.
31
+ * @returns A single character.
32
+ * @throws {RangeError} The value is not an integer within 0 and what the encoding can write,
33
+ * or the encoding is not an {@link EncodingOption} value.
34
+ */
35
+ export declare function getSymbol(p_value: number, p_encoding: EncodingOption): string;
36
+ /**
37
+ * Encodes a grid as one character per square, row by row, '.' between rows.
38
+ * By default ({@link EncodingOption.All}): 0 = blank, 1-9 as digits, 10 and above as letters ('A' = 10).
39
+ * Values above 35 ('Z') are unrepresentable and throw, so rank 5 (25x25) at most.
6
40
  * @param p_puzz The grid to encode, row by row.
7
- * @returns The puzzle code, e.g. "0012.3400...".
41
+ * @param p_encoding The character set to write; {@link maxRank} gives the ranks it fits.
42
+ * Blank (0) is '0' in every encoding.
43
+ * @returns The puzzle code, e.g. "0012.3400..." by default or in {@link EncodingOption.All},
44
+ * or "00AB.CD00..." in {@link EncodingOption.LettersOnly}.
45
+ * @throws {RangeError} A value is not an integer within 0 and what the encoding can write,
46
+ * or the encoding is not an {@link EncodingOption} value.
8
47
  */
9
- export declare function getPuzzleCode(p_puzz: ArrayLike<number>): string;
48
+ export declare function getPuzzleCode(p_puzz: ArrayLike<number>, p_encoding?: EncodingOption): string;
10
49
  /**
11
50
  * Parses a puzzle code back into a grid array.
12
- * @param p_code Text containing the grid as digits and letters ('A' or 'a' = 10);
13
- * every other character is ignored.
51
+ * The encoding can't be told from the code itself (a code without 'O' may or may not be a minus-O one), so it is given.
52
+ * @param p_code Text containing the grid, '0' = blank, letters in either case ('A' or 'a' = 10 by default);
53
+ * every character the encoding doesn't use is ignored, including 'O' in the minus-O encodings.
54
+ * @param p_encoding The character set the code was written in; the default reads it as {@link EncodingOption.All}.
14
55
  * @returns One element per recognised character, in order.
56
+ * @throws {RangeError} The encoding is not an {@link EncodingOption} value.
15
57
  */
16
- export declare function getPuzzleArr(p_code: string): number[];
58
+ export declare function getPuzzleArr(p_code: string, p_encoding?: EncodingOption): number[];
17
59
  export declare function count(p_arr: ArrayLike<number>, p_num: number): number;
18
60
  /**
19
61
  * Fills an array block-wise so that every consecutive block ends up a random permutation of the same value range.
package/dist/utility.js CHANGED
@@ -1,40 +1,133 @@
1
1
  // Copyright (c) 2024-2026 New Media XP. Licensed under the MIT License.
2
2
  /**
3
- * Encodes a grid as one character per square, row by row: 0 = blank, 1-9 as digits,
4
- * 10 and above as letters ('A' = 10), '.' between rows.
5
- * Values above 35 ('Z') are unrepresentable, so rank 5 (25x25) at most.
3
+ * Character sets a puzzle code can be written in. Blank (0) is '0' in every one of them.
4
+ */
5
+ export const EncodingOption = Object.freeze({
6
+ /** 1 to 9 as digits, then 10 to 35 as A to Z. Fits every rank. */
7
+ All: 0,
8
+ /** 1 to 9 as digits, then 10 to 34 as A to Z without O, so it can't be misread as blank '0'. Fits every rank. */
9
+ AllMinusO: 1,
10
+ /** 1 to 9 as digits only. Fits rank 3 and below. */
11
+ DigitsOnly: 2,
12
+ /** 1 to 26 as A to Z. Fits every rank. */
13
+ LettersOnly: 3,
14
+ /** 1 to 25 as A to Z without O, so it can't be misread as blank '0'. Fits every rank. */
15
+ LettersOnlyMinusO: 4,
16
+ });
17
+ // the highest square value each encoding can write, indexed by EncodingOption; blank (0) is '0' on top of these
18
+ const maxValues = [35, 34, 9, 26, 25];
19
+ const encodingNames = Object.keys(EncodingOption);
20
+ function maxValue(p_encoding) {
21
+ const a_maxValue = Number.isInteger(p_encoding) ? maxValues[p_encoding] : undefined;
22
+ if (a_maxValue === undefined)
23
+ throw new RangeError(`unknown encoding ${p_encoding}`);
24
+ return a_maxValue;
25
+ }
26
+ /**
27
+ * The highest rank whose grids an encoding can write; a rank r grid holds values up to r^2.
28
+ * Never above Sudoku.maxRank, since no encoding writes past 35.
29
+ * @param p_encoding The character set to check.
30
+ * @returns 3 for {@link EncodingOption.DigitsOnly}, 5 for every other encoding.
31
+ * @throws {RangeError} The encoding is not an {@link EncodingOption} value.
32
+ */
33
+ export function maxRank(p_encoding) {
34
+ return Math.trunc(Math.sqrt(maxValue(p_encoding)));
35
+ }
36
+ // the letter at index p_letterIdx of A to Z with O skipped; 'A' = 65, 'O' - 'A' = 14
37
+ function letterMinusO(p_letterIdx) {
38
+ return String.fromCharCode(65 + (p_letterIdx < 14 ? p_letterIdx : p_letterIdx + 1));
39
+ }
40
+ /**
41
+ * The character an encoding writes for one square value; blank (0) is '0' in every encoding.
42
+ * @param p_value The square value, from 0 (blank) up to what the encoding can write.
43
+ * @param p_encoding The character set to write it in.
44
+ * @returns A single character.
45
+ * @throws {RangeError} The value is not an integer within 0 and what the encoding can write,
46
+ * or the encoding is not an {@link EncodingOption} value.
47
+ */
48
+ export function getSymbol(p_value, p_encoding) {
49
+ const a_maxValue = maxValue(p_encoding);
50
+ if (!Number.isInteger(p_value) || p_value < 0 || p_value > a_maxValue)
51
+ throw new RangeError(`value ${p_value} is outside 0..${a_maxValue} for ${encodingNames[p_encoding]} encoding`);
52
+ if (p_value === 0)
53
+ return "0";
54
+ // '0' = 48, 'A' - 10 = 55, 'A' - 1 = 64
55
+ switch (p_encoding) {
56
+ case EncodingOption.All:
57
+ return String.fromCharCode(p_value <= 9 ? 48 + p_value : 55 + p_value);
58
+ case EncodingOption.AllMinusO:
59
+ return p_value <= 9 ? String.fromCharCode(48 + p_value) : letterMinusO(p_value - 10);
60
+ case EncodingOption.DigitsOnly:
61
+ return String.fromCharCode(48 + p_value);
62
+ case EncodingOption.LettersOnly:
63
+ return String.fromCharCode(64 + p_value);
64
+ case EncodingOption.LettersOnlyMinusO:
65
+ return letterMinusO(p_value - 1);
66
+ }
67
+ }
68
+ // the value one character code stands for under p_encoding, or -1 when the encoding doesn't use that character
69
+ function getValue(p_char, p_encoding) {
70
+ // '0'..'9' = 48..57, 'A'..'Z' = 65..90, 'a'..'z' = 97..122, 'O' = 79
71
+ if (p_char === 48)
72
+ return 0;
73
+ // ASCII-only case fold; toUpperCase would also turn 'ı' and 'ſ' into letters
74
+ const a_upper = p_char >= 97 && p_char <= 122 ? p_char - 32 : p_char;
75
+ const a_digit = a_upper >= 49 && a_upper <= 57 ? a_upper - 48 : -1;
76
+ const a_letter = a_upper >= 65 && a_upper <= 90 ? a_upper - 65 : -1;
77
+ // O is not a symbol of the minus-O encodings, so it is ignored like any other unused character
78
+ const a_letterMinusO = a_letter < 0 || a_upper === 79 ? -1 : a_upper < 79 ? a_letter : a_letter - 1;
79
+ switch (p_encoding) {
80
+ case EncodingOption.All:
81
+ return a_digit > 0 ? a_digit : a_letter >= 0 ? a_letter + 10 : -1;
82
+ case EncodingOption.AllMinusO:
83
+ return a_digit > 0 ? a_digit : a_letterMinusO >= 0 ? a_letterMinusO + 10 : -1;
84
+ case EncodingOption.DigitsOnly:
85
+ return a_digit;
86
+ case EncodingOption.LettersOnly:
87
+ return a_letter >= 0 ? a_letter + 1 : -1;
88
+ case EncodingOption.LettersOnlyMinusO:
89
+ return a_letterMinusO >= 0 ? a_letterMinusO + 1 : -1;
90
+ }
91
+ }
92
+ /**
93
+ * Encodes a grid as one character per square, row by row, '.' between rows.
94
+ * By default ({@link EncodingOption.All}): 0 = blank, 1-9 as digits, 10 and above as letters ('A' = 10).
95
+ * Values above 35 ('Z') are unrepresentable and throw, so rank 5 (25x25) at most.
6
96
  * @param p_puzz The grid to encode, row by row.
7
- * @returns The puzzle code, e.g. "0012.3400...".
97
+ * @param p_encoding The character set to write; {@link maxRank} gives the ranks it fits.
98
+ * Blank (0) is '0' in every encoding.
99
+ * @returns The puzzle code, e.g. "0012.3400..." by default or in {@link EncodingOption.All},
100
+ * or "00AB.CD00..." in {@link EncodingOption.LettersOnly}.
101
+ * @throws {RangeError} A value is not an integer within 0 and what the encoding can write,
102
+ * or the encoding is not an {@link EncodingOption} value.
8
103
  */
9
- export function getPuzzleCode(p_puzz) {
104
+ export function getPuzzleCode(p_puzz, p_encoding = EncodingOption.All) {
105
+ maxValue(p_encoding);
10
106
  const a_rows = Math.trunc(Math.sqrt(p_puzz.length));
11
107
  let a_puzzleCode = "";
12
108
  for (let i = 0; i < p_puzz.length; ++i) {
13
109
  if (i > 0 && i % a_rows === 0)
14
110
  a_puzzleCode += ".";
15
- const a_value = p_puzz[i];
16
- // 'A' - 10 = 55, '0' = 48
17
- a_puzzleCode += String.fromCharCode(a_value > 9 ? 55 + a_value : 48 + a_value);
111
+ a_puzzleCode += getSymbol(p_puzz[i], p_encoding);
18
112
  }
19
113
  return a_puzzleCode;
20
114
  }
21
115
  /**
22
116
  * Parses a puzzle code back into a grid array.
23
- * @param p_code Text containing the grid as digits and letters ('A' or 'a' = 10);
24
- * every other character is ignored.
117
+ * The encoding can't be told from the code itself (a code without 'O' may or may not be a minus-O one), so it is given.
118
+ * @param p_code Text containing the grid, '0' = blank, letters in either case ('A' or 'a' = 10 by default);
119
+ * every character the encoding doesn't use is ignored, including 'O' in the minus-O encodings.
120
+ * @param p_encoding The character set the code was written in; the default reads it as {@link EncodingOption.All}.
25
121
  * @returns One element per recognised character, in order.
122
+ * @throws {RangeError} The encoding is not an {@link EncodingOption} value.
26
123
  */
27
- export function getPuzzleArr(p_code) {
124
+ export function getPuzzleArr(p_code, p_encoding = EncodingOption.All) {
125
+ maxValue(p_encoding);
28
126
  const a_puzzle = [];
29
127
  for (let i = 0; i < p_code.length; ++i) {
30
- const c = p_code.charCodeAt(i);
31
- // '0'..'9' = 48..57, 'A'..'Z' = 65..90, 'a'..'z' = 97..122
32
- if (c >= 48 && c <= 57)
33
- a_puzzle.push(c - 48);
34
- else if (c >= 65 && c <= 90)
35
- a_puzzle.push(c - 55);
36
- else if (c >= 97 && c <= 122)
37
- a_puzzle.push(c - 87);
128
+ const a_value = getValue(p_code.charCodeAt(i), p_encoding);
129
+ if (a_value !== -1)
130
+ a_puzzle.push(a_value);
38
131
  }
39
132
  return a_puzzle;
40
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nmx.sudokugen.library",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Sudoku generator and solver: unique-solution puzzle creation (optionally seeded), solving, validation, conflict reporting and isomorphic shuffling.",
5
5
  "keywords": [
6
6
  "sudoku",
@@ -8,14 +8,14 @@
8
8
  "solver",
9
9
  "generator"
10
10
  ],
11
- "homepage": "https://github.com/newmediaxp/sudokugen/tree/main/js/nmx.sudokugen.library#readme",
11
+ "homepage": "https://github.com/newmediaxp/sudokugen/tree/main/javascript/nmx.sudokugen.library#readme",
12
12
  "bugs": {
13
13
  "url": "https://github.com/newmediaxp/sudokugen/issues"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/newmediaxp/sudokugen.git",
18
- "directory": "js/nmx.sudokugen.library"
18
+ "directory": "javascript/nmx.sudokugen.library"
19
19
  },
20
20
  "license": "MIT",
21
21
  "author": "New Media XP <contact@newmediaxp.com> (https://www.newmediaxp.com)",