git clone https://github.com/Cantara/lib-electronic-components
T=$(mktemp -d) && git clone --depth=1 https://github.com/Cantara/lib-electronic-components "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/mpn-normalization" ~/.claude/skills/cantara-lib-electronic-components-mpn-normalization && rm -rf "$T"
.claude/skills/mpn-normalization/SKILL.mdMPN Normalization
Use this skill when working with MPN (Manufacturer Part Number) normalization, package suffix handling, and component equivalence checking.
Core Methods
1. stripPackageSuffix() - Remove Packaging Codes
Purpose: Strip manufacturer-specific package suffixes to get the base component part number.
Supported patterns:
| Pattern | Delimiter | Example | Manufacturers | Use Case |
|---|---|---|---|---|
| Plus suffix | | MAX3483EESA**+** → MAX3483EESA | Maxim, Analog Devices | Lead-free indicator |
| Hash suffix | | LTC2053HMS8**#PBF** → LTC2053HMS8 | Linear Technology | RoHS, Tape & Reel (#PBF, #TR, #TRPBF) |
| Slash suffix | | TJA1050T**/CM,118** → TJA1050T | NXP | Ordering codes (/CM,118, /SN) |
| Comma suffix | | NC7WZ04**,315** → NC7WZ04 | Various | Generic ordering codes |
API:
String base = MPNUtils.stripPackageSuffix("MAX3483EESA+"); // Returns: "MAX3483EESA"
Implementation (MPNUtils.java lines 565-597):
public static String stripPackageSuffix(String mpn) { if (mpn == null || mpn.trim().isEmpty()) { return ""; } String trimmed = mpn.trim(); // Pattern 1: + suffix (Maxim/Analog Devices lead-free) if (trimmed.endsWith("+")) { return trimmed.substring(0, trimmed.length() - 1); } // Pattern 2: # suffix (Linear Tech: #PBF, #TR, #TRPBF) int hashIndex = trimmed.indexOf('#'); if (hashIndex > 0) { return trimmed.substring(0, hashIndex); } // Pattern 3: / suffix (NXP ordering codes: /CM,118) int slashIndex = trimmed.indexOf('/'); if (slashIndex > 0) { return trimmed.substring(0, slashIndex); } // Pattern 4: , suffix (some ordering codes) int commaIndex = trimmed.indexOf(','); if (commaIndex > 0) { return trimmed.substring(0, commaIndex); } // No recognized suffix return trimmed; }
Gotcha: Single-letter suffixes are NOT stripped to avoid false positives:
MPNUtils.stripPackageSuffix("NC7WZ04G"); // Returns: "NC7WZ04G" (not "NC7WZ04") // "G" could be part of base MPN, not a packaging suffix
2. getSearchVariations() - Generate Search Variants
Purpose: Generate MPN variations for datasheet searches, component lookups, and supplier matching.
Use cases:
- Datasheet searches: Try both "MAX3483EESA+" and "MAX3483EESA" for better results
- Component deduplication: Recognize variants as same component
- Supplier matching: Match parts despite different packaging codes
API:
List<String> variations = MPNUtils.getSearchVariations("LTC2053HMS8#PBF"); // Returns: ["LTC2053HMS8#PBF", "LTC2053HMS8"]
Implementation (MPNUtils.java lines 619-636):
public static List<String> getSearchVariations(String mpn) { List<String> variations = new ArrayList<>(); if (mpn == null || mpn.trim().isEmpty()) { return variations; } // Always include original variations.add(mpn); // Add base part if different String base = stripPackageSuffix(mpn); if (!base.isEmpty() && !base.equals(mpn)) { variations.add(base); } return variations; }
Examples:
MPNUtils.getSearchVariations("MAX3483EESA+"); // → ["MAX3483EESA+", "MAX3483EESA"] MPNUtils.getSearchVariations("LTC2053HMS8#PBF"); // → ["LTC2053HMS8#PBF", "LTC2053HMS8"] MPNUtils.getSearchVariations("ADS1115"); // → ["ADS1115"] // No variations if no suffix
3. isEquivalentMPN() - Component Equivalence Check
Purpose: Check if two MPNs refer to the same base component, ignoring packaging differences.
Use cases:
- BOM validation: Verify supplier parts match design MPNs
- Component deduplication: Merge duplicate entries in inventory
- Cross-referencing: Identify same component across different suppliers
API:
boolean equiv = MPNUtils.isEquivalentMPN("LTC2053HMS8#PBF", "LTC2053HMS8#TR"); // Returns: true (same base component, different tape/reel)
Implementation (MPNUtils.java lines 656-669):
public static boolean isEquivalentMPN(String mpn1, String mpn2) { if (mpn1 == null || mpn2 == null) { return false; } String base1 = stripPackageSuffix(mpn1); String base2 = stripPackageSuffix(mpn2); if (base1.isEmpty() || base2.isEmpty()) { return false; } return base1.equalsIgnoreCase(base2); // Case-insensitive comparison }
Examples:
// ✅ Equivalent (same base, different packaging) MPNUtils.isEquivalentMPN("MAX3483EESA+", "MAX3483EESA"); // → true MPNUtils.isEquivalentMPN("LTC2053HMS8#PBF", "LTC2053HMS8#TR"); // → true (same chip, different tape/reel) MPNUtils.isEquivalentMPN("TJA1050T/CM,118", "TJA1050T/SN"); // → true (same part, different ordering codes) // ❌ Not equivalent (different base parts) MPNUtils.isEquivalentMPN("NC7WZ485M8X", "NC7WZ240"); // → false (different chips) MPNUtils.isEquivalentMPN("LM358", "LM324"); // → false (different op-amp configurations: dual vs quad)
4. getPackageSuffix() - Extract Package Suffix
Purpose: Extract the package suffix from an MPN, if present.
API:
Optional<String> suffix = MPNUtils.getPackageSuffix("MAX3483EESA+"); // Returns: Optional.of("+") Optional<String> suffix = MPNUtils.getPackageSuffix("ADS1115"); // Returns: Optional.empty()
Implementation (MPNUtils.java lines 685-696):
public static Optional<String> getPackageSuffix(String mpn) { if (mpn == null || mpn.trim().isEmpty()) { return Optional.empty(); } String base = stripPackageSuffix(mpn); if (base.equals(mpn.trim())) { return Optional.empty(); // No suffix found } return Optional.of(mpn.trim().substring(base.length())); }
Examples:
MPNUtils.getPackageSuffix("MAX3483EESA+"); // → Optional.of("+") MPNUtils.getPackageSuffix("LTC2053HMS8#PBF"); // → Optional.of("#PBF") MPNUtils.getPackageSuffix("TJA1050T/CM,118"); // → Optional.of("/CM,118") MPNUtils.getPackageSuffix("ADS1115IDGSR"); // → Optional.empty()
5. normalize() - Case and Special Character Removal
Purpose: Normalize MPNs for consistent matching by removing special characters and converting to uppercase.
API:
String normalized = MPNUtils.normalize("LM358-N"); // Returns: "LM358N"
Implementation (MPNUtils.java lines 56-61):
private static final Pattern SPECIAL_CHARS = Pattern.compile("[^A-Z0-9]"); public static String normalize(String mpn) { if (mpn == null || mpn.trim().isEmpty()) { return ""; } return SPECIAL_CHARS.matcher(mpn.trim().toUpperCase()).replaceAll(""); }
Critical for position-based extraction:
// ❌ WRONG: Using MPN directly with hyphens String mpn = "ATMEGA328P-PU"; char packageChar = mpn.charAt(mpn.length() - 2); // Returns 'P' (from "-PU"), not package! // ✅ CORRECT: Normalize first String normalized = MPNUtils.normalize("ATMEGA328P-PU"); // → "ATMEGA328PPU" // Now position-based extraction works correctly
Examples:
MPNUtils.normalize("LM358-N"); // → "LM358N" MPNUtils.normalize("stm32f103c8t6"); // → "STM32F103C8T6" MPNUtils.normalize(" LM7805/CT "); // → "LM7805CT" MPNUtils.normalize("MAX3483+"); // → "MAX3483+" // Plus is removed by normalization (not A-Z0-9) // Wait, that's wrong - let me check...
IMPORTANT NOTE:
normalize() removes ALL non-alphanumeric characters, including package suffix delimiters (+, #, /, ,). This makes it INCOMPATIBLE with stripPackageSuffix() which relies on delimiters.
Use normalize() for:
- MPN comparison (case-insensitive, whitespace-agnostic)
- Manufacturer detection
- Type detection
DO NOT use normalize() before stripPackageSuffix():
// ❌ WRONG: normalize() removes delimiters String base = MPNUtils.stripPackageSuffix(MPNUtils.normalize("MAX3483+")); // → "MAX3483" (normalize removed +, stripPackageSuffix does nothing) // ✅ CORRECT: stripPackageSuffix first, then normalize if needed String base = MPNUtils.stripPackageSuffix("MAX3483+"); // → "MAX3483" String normalized = MPNUtils.normalize(base); // → "MAX3483"
Unicode Gotcha: µ → Μ Uppercasing Issue
Problem: The micro sign µ (U+00B5) becomes Greek MU Μ (U+039C) when uppercased, breaking parsing.
The Issue
String mpn = "10µF"; String upper = mpn.toUpperCase(); // upper = "10ΜF" (Greek MU Μ, NOT Latin M!) // Parsing logic looking for "u" or "µ" fails if (upper.contains("UF")) { // ❌ FALSE - contains "ΜF" (Greek MU) // Parse microfarads }
Character codes:
- Micro sign: µ (U+00B5)
- Greek MU (uppercase): Μ (U+039C)
- Latin M (uppercase): M (U+004D)
These are DIFFERENT Unicode characters!
Impact on CapacitorSimilarityCalculator
parseCapacitanceValue() (lines 189-192):
// ✅ CORRECT: Replace µ/Μ with 'u' BEFORE normalizing String normalized = value.replace("µ", "u").replace("Μ", "u"); normalized = normalizeValue(normalized); if (normalized == null) { return null;
Why this matters:
// Input: "10µF" // Step 1: Replace µ → u: "10uF" // Step 2: toUpperCase(): "10UF" // Step 3: Parse successfully // ❌ WRONG: toUpperCase() first // Input: "10µF" // Step 1: toUpperCase(): "10ΜF" (Greek MU!) // Step 2: Try to parse: FAILS - looking for "UF", finds "ΜF"
Solution Pattern
Always replace µ/Μ with 'u' BEFORE calling toUpperCase() or normalize():
// ✅ CORRECT Pattern public static String parseValue(String value) { // 1. Replace micro variants with 'u' FIRST String cleaned = value.replace("µ", "u").replace("Μ", "u"); // 2. NOW safe to uppercase String upper = cleaned.toUpperCase(); // 3. Parse normally if (upper.endsWith("UF")) { // Parse microfarads } } // ❌ WRONG Pattern public static String parseValue(String value) { String upper = value.toUpperCase(); // µ → Μ (Greek MU) // Parsing will fail! }
Real-World Examples
// Capacitor MPNs with micro sign "GRM188R71H103KA01D" // 10nF (no micro sign) "C1206C105K4RAC" // 1µF (might be written as "1uF" or "1µF") // If user input contains µ: "1µF" → replace µ with u → "1uF" → uppercase → "1UF" → parse ✅ // If uppercased first: "1µF" → uppercase → "1ΜF" → parse fails ❌ (looking for "UF", finds "ΜF")
Testing Strategy
ParameterizedTest Examples
Test all 4 suffix patterns:
@ParameterizedTest @CsvSource({ "MAX3483EESA+, MAX3483EESA", // Plus suffix "LTC2053HMS8#PBF, LTC2053HMS8", // Hash suffix "TJA1050T/CM,118, TJA1050T", // Slash suffix "NC7WZ04,315, NC7WZ04" // Comma suffix }) void testStripPackageSuffix(String input, String expected) { assertEquals(expected, MPNUtils.stripPackageSuffix(input)); }
Test edge cases:
@Test void testStripPackageSuffix_EdgeCases() { assertEquals("", MPNUtils.stripPackageSuffix(null)); assertEquals("", MPNUtils.stripPackageSuffix("")); assertEquals("", MPNUtils.stripPackageSuffix(" ")); assertEquals("TEST", MPNUtils.stripPackageSuffix(" TEST ")); } @Test void testStripPackageSuffix_SingleCharacter() { // Single character - should NOT strip (ambiguous) assertEquals("NC7WZ04G", MPNUtils.stripPackageSuffix("NC7WZ04G")); }
Test search variations:
@Test void testGetSearchVariations_WithSuffix() { List<String> variations = MPNUtils.getSearchVariations("MAX3483EESA+"); assertEquals(2, variations.size()); assertTrue(variations.contains("MAX3483EESA+")); assertTrue(variations.contains("MAX3483EESA")); } @Test void testGetSearchVariations_NoSuffix() { List<String> variations = MPNUtils.getSearchVariations("ADS1115"); assertEquals(1, variations.size()); assertTrue(variations.contains("ADS1115")); }
Test equivalence:
@ParameterizedTest @CsvSource({ "MAX3483EESA+, MAX3483EESA, true", "LTC2053HMS8#PBF, LTC2053HMS8#TR, true", "TJA1050T/CM,118, TJA1050T/SN, true", "NC7WZ485M8X, NC7WZ240, false", "LM358, LM324, false" }) void testIsEquivalentMPN(String mpn1, String mpn2, boolean expected) { assertEquals(expected, MPNUtils.isEquivalentMPN(mpn1, mpn2)); }
Manufacturer-Specific Notes
Maxim Integrated / Analog Devices
Suffix:
+ (plus sign)
- Meaning: Lead-free, RoHS compliant
- Examples:
- MAX3483EESA+ → MAX3483EESA
- MAX485ESA+ → MAX485ESA
- ADS1115IDGSR+ → ADS1115IDGSR
Linear Technology (now Analog Devices)
Suffix:
#PBF, #TR, #TRPBF
- Meaning:
- PBF = Lead-free (Pb-Free)
- TR = Tape and Reel
- TRPBF = Tape and Reel, Lead-free
- Examples:
- LTC2053HMS8#PBF → LTC2053HMS8
- LT1117CST#TR → LT1117CST
- LTC2053HMS8#TRPBF → LTC2053HMS8
NXP Semiconductors
Suffix:
/CM,118, /SN
- Meaning: Ordering codes, package variants
- Examples:
- TJA1050T/CM,118 → TJA1050T
- MCP2551-I/SN → MCP2551-I
Various Manufacturers
Suffix:
,315, ,118
- Meaning: Generic ordering codes
- Examples:
- NC7WZ04,315 → NC7WZ04
Gotchas
1. First Delimiter Wins
If multiple delimiters exist, the first match is used:
MPNUtils.stripPackageSuffix("PART#ABC/XYZ"); // Returns: "PART" (stops at #, ignores /)
2. Conservative Design
Single-letter suffixes are NOT stripped to avoid false positives:
MPNUtils.stripPackageSuffix("NC7WZ04G"); // Returns: "NC7WZ04G" (not "NC7WZ04") // "G" could be part of base MPN
3. No Manufacturer-Specific Logic
The stripPackageSuffix() method uses generic patterns, not ManufacturerHandler integration:
// Works for known patterns MPNUtils.stripPackageSuffix("MAX3483+"); // → "MAX3483" // Doesn't understand manufacturer-specific suffixes MPNUtils.stripPackageSuffix("LM7805CT"); // → "LM7805CT" (CT not recognized) // "CT" is a TI voltage regulator package suffix, but not a recognized delimiter
For manufacturer-specific package extraction, use
ManufacturerHandler.extractPackageCode() instead.
4. Case-Insensitive Equivalence
isEquivalentMPN() uses case-insensitive comparison:
MPNUtils.isEquivalentMPN("lm358n", "LM358N"); // → true
See Also
- Base skill for general component work/component
- Handler patterns (uses normalization for package extraction)/handler-pattern-design
(lines 54-697) - Implementation detailsMPNUtils.java
- 32 comprehensive testsMPNPackageSuffixTest.java
- MPN Package Suffix Support sectionCLAUDE.md
- PR #128 (MPN package suffix support)HISTORY.md