datasheet fix

This commit is contained in:
Bane Stojkovic
2026-11-06 09:57:02 +01:00
parent c47ff3a8fc
commit 396c150472
5 changed files with 159 additions and 51 deletions
@@ -1404,8 +1404,8 @@ WIMA MKS-4,250 V,82 uF B,57,35,52.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,250 V,100 uF A,57,45,52.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,250 V,120 uF B,57,45,52.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,250 V,150 uF,57,45,52.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.01µF A,10,3,7.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.01µF B,13,4,10,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.01 uF A,10,3,7.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.01 uF B,13,4,10,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.015 uF A,10,3,7.5,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.015 uF B,13,4,10,#FF6666,#A03140,#000000,BOX
WIMA MKS-4,400 V,0.022 uF A,10,4,7.5,#FF6666,#A03140,#000000,BOX
@@ -0,0 +1,133 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.components;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.diylc.TestConfiguration;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDatasheetSupport;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.presenter.DatasheetService;
import org.diylc.utils.ReflectionUtils;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Applies every row of every datasheet to its component, so a malformed row fails the build rather
* than the user's Apply Model, or an AI edit that picks the model. The components are found through
* the class index, so a new datasheet is covered without touching this test.
*/
public class DatasheetRowsTest {
@BeforeClass
public static void setUp() {
TestConfiguration.initialize();
}
@Test
public void everyRowApplies() throws ReflectiveOperationException {
List<String> failures = new ArrayList<String>();
for (Class<? extends IDIYComponent<?>> type : datasheetComponents()) {
List<String[]> datasheet = DatasheetService.getInstance().loadDatasheet(type);
Assert.assertNotNull(type.getSimpleName(), datasheet);
Assert.assertFalse(type.getSimpleName(), datasheet.isEmpty());
for (String[] row : datasheet) {
IDatasheetSupport component =
(IDatasheetSupport) type.getDeclaredConstructor().newInstance();
try {
component.applyModel(row);
} catch (RuntimeException e) {
failures.add(type.getSimpleName() + ": " + String.join(",", row) + " -> " + e);
}
}
}
Assert.assertTrue(String.join("\n", failures), failures.isEmpty());
}
/**
* DatasheetService decodes with the platform charset, where a latin-1 byte such as the micro
* sign turns into U+FFFD instead of failing, so units are written as uF.
*/
@Test
public void datasheetsAreAscii() throws IOException {
List<String> failures = new ArrayList<String>();
for (Class<? extends IDIYComponent<?>> type : datasheetComponents()) {
String fileName = type.getSimpleName() + ".datasheet";
try (InputStream in = type.getResourceAsStream("/datasheets/" + fileName)) {
Assert.assertNotNull(fileName, in);
int line = 1;
int b;
while ((b = in.read()) != -1) {
if (b == '\n') {
line++;
} else if (b > 0x7f) {
failures.add(fileName + ":" + line);
}
}
}
}
Assert.assertTrue("non-ASCII bytes at " + failures, failures.isEmpty());
}
/** A datasheet is found by the class name, so a file no component claims is never loaded. */
@Test
public void everyDatasheetFileHasAComponent() throws URISyntaxException {
URL folder = DatasheetRowsTest.class.getResource("/datasheets");
Assert.assertNotNull(folder);
Set<String> claimed = new HashSet<String>();
for (Class<? extends IDIYComponent<?>> type : datasheetComponents()) {
claimed.add(type.getSimpleName() + ".datasheet");
}
for (String fileName : new File(folder.toURI()).list()) {
Assert.assertTrue(fileName + " belongs to no datasheet component",
claimed.contains(fileName));
}
}
@SuppressWarnings("unchecked")
private static List<Class<? extends IDIYComponent<?>>> datasheetComponents() {
List<Class<? extends IDIYComponent<?>>> types =
new ArrayList<Class<? extends IDIYComponent<?>>>();
for (Class<?> type : ReflectionUtils.getIndexedClasses(IDIYComponent.class)) {
ComponentDescriptor descriptor = type.getAnnotation(ComponentDescriptor.class);
if (descriptor != null && descriptor.enableDatasheet()
&& !Modifier.isAbstract(type.getModifiers())) {
Assert.assertTrue(type.getSimpleName() + " enables a datasheet without IDatasheetSupport",
IDatasheetSupport.class.isAssignableFrom(type));
types.add((Class<? extends IDIYComponent<?>>) type);
}
}
Assert.assertFalse("no datasheet components in the class index", types.isEmpty());
return types;
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.9.0</version>
<version>6.10.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
+19 -44
View File
@@ -209,7 +209,7 @@ Two cautions, both learned from §6.5:
- **The column format is positional and unforgiving.** `CapacitorDatasheetService` reads
`model[1].split(" ")[0..1]` for voltage and `model[2].split(" ")[0..1]` for value, so a cell
must be `"<number> <unit>"` with exactly one space. `4.7uF` throws
`ArrayIndexOutOfBoundsException`; there is no validation anywhere.
`ArrayIndexOutOfBoundsException`; `DatasheetRowsTest` catches it.
- **`DatasheetService` reads with `new InputStreamReader(in)` and no charset**, so files are decoded
with the platform default. Keep datasheet files ASCII — write `uF`, never `µF`. Per JEP 400 the
default is UTF-8 on the supported JVM, so a latin-1 byte becomes U+FFFD rather than failing loudly.
@@ -282,7 +282,7 @@ block for the unreleased version. `NEW_FEATURE` for components and variants, `BU
Enum constants, catalogue data, building blocks and uncommenting. Cheapest fidelity per hour in
the roadmap, and where D6 and D7 send most of the work.
§6.1 and §6.5 are correctness fixes and should go first regardless of the rest.
§6.1 is a correctness fix and should go first regardless of the rest.
### 6.1 Restore the `ElectrolyticCanCapacitor` alias — **bug, not a gap**
@@ -385,36 +385,12 @@ value `"<number> <unit>"`.
**~6-10 h each, `RadialCeramicDiskCapacitor` first.**
### 6.5 Fix six malformed datasheet rows — **bug**
### 6.5 Fix six malformed datasheet rows — **done**
Six of the 3,539 rows have a value cell the parser cannot read. Found by validating every row
against `parseCapacitorDatasheet`'s own logic:
| File | Rows | Cell | Failure |
|---|---|---|---|
| `RadialElectrolytic.datasheet` | 4 (Elna Silmic II 4.7 uF at 25/35/50/63 V) | `4.7uF` | no space, so `split(" ")` yields one element and `capacitanceParts[1]` throws `ArrayIndexOutOfBoundsException` |
| `RadialFilmCapacitor.datasheet` | 2 (WIMA MKS-4 400 V) | `0.01µF A`, `0.01µF B` | latin-1 `0xb5` in a file read with the platform charset (§5.3), no space, plus an ` A`/` B` suffix no other row has; `parseCapacitance` throws `IllegalArgumentException` |
User-visible effect: picking any of those six models from *Apply Model* throws instead of applying.
Latent effect: the same cells break `DatasheetService.lookup`, whose `Double.parseDouble` runs over
every row in a `model|voltage` bucket. Three of the five affected buckets have the bad row first, so
the whole bucket would throw — **80 rows across 5 buckets, including all 41 WIMA MKS-4 400 V rows**.
That path has no production caller today (§5.3), so this is a trap set for whoever wires up
value-based auto-sizing, not a live fault.
1. Rewrite the four `4.7uF` cells as `4.7 uF`.
2. Replace the two `0.01µF A` / `0.01µF B` cells with ASCII. The ` A`/` B` suffix looks like an attempt
to give one value two body sizes (10x3 at 7.5 mm pitch, 13x4 at 10 mm pitch) — a real need the
positional format cannot express, since model and voltage are the whole lookup key. **Recommend
splitting them into two models**, `WIMA MKS-4` and `WIMA MKS-4 (10mm)`, matching how
`Epcos MKT (5mm)` / `(7.5mm)` / `(10mm)` and the WIMA 2.5 mm/5 mm entries already encode pitch in
the model name.
3. Add this row validation as a test beside `FactoryVariantsTest` — same shape, same
collect-all-problems-then-assert style — so the next malformed cell fails the build. **This is
the part that matters**; the six fixes are typos, the missing validation is why they survived.
**~3 h including the test.**
The four Elna Silmic II `4.7uF` cells and the two WIMA MKS-4 `0.01µF A`/`B` cells now read `4.7 uF`
and `0.01 uF A`/`B`. The ` A`/` B` suffix stays: it is how the other WIMA rows give one value two
case sizes, and only the value before it is applied. `DatasheetRowsTest` applies every row of every
datasheet and rejects non-ASCII bytes, so the next malformed cell fails the build.
### 6.6 Make factory building blocks reach existing users — **done**
@@ -570,20 +546,19 @@ joins the §6.7 list rather than this one once someone asks for it.
| Step | Items | Rough effort |
|---|---|---|
| 1 | §6.5 six malformed datasheet rows, plus the validation test | 3 h |
| 2 | §6.1 alias fix, verified by regression run | 2 h |
| 3 | §6.7 push-pull pot block, as the D7 worked example | 3 h |
| 4 | §6.2 revive three, decide on the fourth | 3 h |
| 5 | §6.3 UX4, then UX5 / B8G / B9D | 6-8 h |
| 6 | §6.4 datasheets, `RadialCeramicDiskCapacitor` first | 6-10 h each |
| 7 | §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses | 2-3 h each |
| 8 | §7.1 `Transformer`, with §7.3 Choke as its third type | 20-24 h |
| 9 | §7.2 Speaker, physical plus symbol | 10-12 h |
| 10 | §7.4 Footswitch | 10-12 h |
| 11 | §7.5 `Photoresistor`, then the Vactrol block | 6 h |
| 1 | §6.1 alias fix, verified by regression run | 2 h |
| 2 | §6.7 push-pull pot block, as the D7 worked example | 3 h |
| 3 | §6.2 revive three, decide on the fourth | 3 h |
| 4 | §6.3 UX4, then UX5 / B8G / B9D | 6-8 h |
| 5 | §6.4 datasheets, `RadialCeramicDiskCapacitor` first | 6-10 h each |
| 6 | §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses | 2-3 h each |
| 7 | §7.1 `Transformer`, with §7.3 Choke as its third type | 20-24 h |
| 8 | §7.2 Speaker, physical plus symbol | 10-12 h |
| 9 | §7.4 Footswitch | 10-12 h |
| 10 | §7.5 `Photoresistor`, then the Vactrol block | 6 h |
Steps 1-4 are about a day and a half: both known bugs cleared and the highest-demand composable
shipped as data rather than as a class. Step 8 is the one that matters most.
Steps 1-3 are about a day: the alias bug cleared and the highest-demand composable shipped as data
rather than as a class. Step 7 is the one that matters most.
Three items that looked like components when this roadmap started are now data (§6.7, §7.6, §8.9),
and one is split into a primitive plus a block (§7.5). That is D7 working as intended; expect the