# NumericFilter — Class Detailed Design [41 LOC]

| Field | Value |
|-------|-------|
| Fully Qualified Name | `com.github.blaxk3.ui.NumericFilter` |
| Layer | Controller (UI Input Filtering) |
| Module | `com.github.blaxk3.ui` |

## Class Overview

`NumericFilter` is an inner class nested within the `UI` class that extends `javax.swing.text.DocumentFilter`. Its purpose is to restrict user text input into Swing text components so that only numeric characters and a single decimal point (`.`) are accepted. It intercepts all text modification operations — inserts and replaces — through the `DocumentFilter` API, validates the resulting text via the private `isValid()` method, and conditionally delegates to the parent class to apply the change. This ensures that the associated text field (likely a currency-amount input) never contains non-numeric content. The class follows the filter pattern and embodies the guard clause design idiom.

---

## Method: insertString(FilterBypass fb, int offset, String string, AttributeSet attr)

### 1. Role
Intercepts insert operations on a bound `JTextComponent` document, constructs the hypothetical resulting text, validates it, and either applies the insertion or silently discards it.

### 2. Processing Pattern

```mermaid
flowchart TD
    START["Start: insertString called"] --> DOC["Get Document from FilterBypass"]
    DOC --> BUILD["Build StringBuilder with current + new text"]
    BUILD --> VALID["Call isValid(sb.toString())"]
    VALID --> CHECK{"isValid passes?"}
    CHECK -->|true| SUPER["super.insertString(fb, offset, string, attr)"]
    CHECK -->|false| END["End - no change applied"]
    SUPER --> END
```

### 3. Parameter Analysis

| Parameter | Type | Description |
|-----------|------|-------------|
| `fb` | `FilterBypass` | The bypass object that allows the caller to perform the actual document modification through `super` calls. Passed unchanged to `super.insertString()` on success. |
| `offset` | `int` | The position in the document where the insertion will occur. Used both to build the hypothetical full text and to pass to the parent's `insertString()`. |
| `string` | `String` | The text being inserted. If null, it is treated as an empty string by the DocumentFilter contract. |
| `attr` | `AttributeSet` | The attribute set of the inserted text (e.g., font style). Passed to `super.insertString()` on success. |

### 4. CRUD Operations / Called Services

| Operation | Target | Details |
|-----------|--------|---------|
| Read | `Document.getText()` | Retrieves the entire current document content via `fb.getDocument().getText(0, doc.getLength())`. |
| Validate | `NumericFilter.isValid(String)` | Validates the hypothetical post-insertion text. |
| Write | `super.insertString(...)` | Applies the insertion to the document if validation passes. |

### 5. Dependency Trace

| Direction | Method/Class | Notes |
|-----------|-------------|-------|
| Calls | `isValid(String)` | Core validation — determines whether the inserted text is acceptable. |
| Called by | `javax.swing.text.Document` | Invoked automatically by the Swing text system whenever a user or program attempts to insert text into a bound `JTextComponent` (e.g., `JTextField`, `JTextArea`). |
| Extends | `javax.swing.text.DocumentFilter` | Inherits the `DocumentFilter` contract; `insertString` is one of the three abstract methods that must be implemented. |

### 6. Per-Branch Detail Blocks

**Block A: Construct full text and validate**

1. Obtain the current `Document` object from the `FilterBypass`.
2. Create a `StringBuilder` and append the entire current document content (`doc.getText(0, doc.getLength())`).
3. Insert the new string at the specified `offset` into the `StringBuilder`.
4. Pass the built string to `isValid(sb.toString())`.

- At this point the code has no early return — it proceeds to the conditional gate.

**Block B: Validation passes (text is numeric or empty)**

1. Call `super.insertString(fb, offset, string, attr)` to perform the actual insertion in the underlying document.
2. The `FilterBypass` ensures the insertion respects the filter's own constraints (no infinite recursion).
3. Control returns to the caller (Swing text system).

**Block C: Validation fails (text contains non-numeric characters)**

1. The method body falls through without calling `super.insertString()`.
2. The insertion is silently dropped — the user sees no change in the text field.
3. No exception is thrown; the document remains in its pre-call state.

---

## Method: replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)

### 1. Role
Intercepts replace operations (delete + insert combined) on a bound `JTextComponent` document, constructs the hypothetical resulting text, validates it, and either applies the replacement or silently discards it.

### 2. Processing Pattern

```mermaid
flowchart TD
    START["Start: replace called"] --> DOC["Get Document from FilterBypass"]
    DOC --> BUILD["Build StringBuilder with current text"]
    BUILD --> REPLACE["Replace range at offset/length with new text"]
    REPLACE --> VALID["Call isValid(sb.toString())"]
    VALID --> CHECK{"isValid passes?"}
    CHECK -->|true| SUPER["super.replace(fb, offset, length, text, attrs)"]
    CHECK -->|false| END["End - no change applied"]
    SUPER --> END
```

### 3. Parameter Analysis

| Parameter | Type | Description |
|-----------|------|-------------|
| `fb` | `FilterBypass` | The bypass object that allows the caller to perform the actual document modification through `super` calls. |
| `offset` | `int` | The start position in the document of the range to replace. |
| `length` | `int` | The number of characters to remove starting at `offset`. A length of 0 means an insert-only operation. |
| `text` | `String` | The replacement text. If null, the range is simply deleted. |
| `attrs` | `AttributeSet` | The attribute set of the replacement text. |

### 4. CRUD Operations / Called Services

| Operation | Target | Details |
|-----------|--------|---------|
| Read | `Document.getText()` | Retrieves the entire current document content via `fb.getDocument().getText(0, doc.getLength())`. |
| Mutate | `StringBuilder.replace(int, int, String)` | Replaces the substring from `offset` to `offset + length` with `text`. |
| Validate | `NumericFilter.isValid(String)` | Validates the hypothetical post-replacement text. |
| Write | `super.replace(...)` | Applies the replacement to the document if validation passes. |

### 5. Dependency Trace

| Direction | Method/Class | Notes |
|-----------|-------------|-------|
| Calls | `isValid(String)` | Core validation — determines whether the replaced text is acceptable. |
| Called by | `javax.swing.text.Document` | Invoked automatically by the Swing text system whenever a user selects text and types, uses cut/copy/paste, or a program calls `Document.replace()` on a bound component. |
| Extends | `javax.swing.text.DocumentFilter` | Inherits the `DocumentFilter` contract; `replace` is the more general of the two filtering methods. |

### 6. Per-Branch Detail Blocks

**Block A: Construct full text and validate**

1. Obtain the current `Document` object from the `FilterBypass`.
2. Create a `StringBuilder` and append the entire current document content.
3. Replace the range `[offset, offset + length)` with the new `text` via `sb.replace(offset, offset + length, text)`.
4. Pass the built string to `isValid(sb.toString())`.

- This approach mirrors `insertString` but uses `StringBuilder.replace()` instead of `insert()` to account for the deletion of existing characters.

**Block B: Validation passes**

1. Call `super.replace(fb, offset, length, text, attrs)` to perform the actual replacement.
2. Control returns to the Swing text system.

**Block C: Validation fails**

1. The method body falls through without calling `super.replace()`.
2. The replacement is silently dropped.
3. No exception is thrown.

---

## Method: isValid(String text)

### 1. Role
Validates whether a string represents a valid numeric value according to the filter's rules: the string may be empty, and if non-empty it must consist only of ASCII digits and at most one decimal point (`.`). Returns `true` if the text passes, `false` otherwise.

### 2. Processing Pattern

```mermaid
flowchart TD
    START["Start: isValid(text)"] --> CHECK_EMPTY{"text.isEmpty()?"}
    CHECK_EMPTY -->|true| RET_TRUE["Return true"]
    CHECK_EMPTY -->|false| INIT["decimalCount = 0"]
    INIT --> LOOP["For each character in text"]
    LOOP --> DOT{"ch == '.'?"}
    DOT -->|true| DCHECK{"decimalCount != 1?"}
    DCHECK -->|true| INCR["decimalCount++"]
    DCHECK -->|false| PASS_DOT["Continue (second dot already counted)"]
    INCR --> PASS_DOT
    DOT -->|false| ISDIGIT{"Character.isDigit(ch)?"}
    ISDIGIT -->|false| RET_FALSE["Return false"]
    ISDIGIT -->|true| NEXT["Next character"]
    NEXT --> LOOP
    PASS_DOT --> NEXT
    LOOP --> DONE["Loop finished"]
    DONE --> RET_VALID["Return true"]
    RET_VALID --> END["End"]
    RET_FALSE --> END
    RET_TRUE --> END
```

### 3. Parameter Analysis

| Parameter | Type | Description |
|-----------|------|-------------|
| `text` | `String` | The full hypothetical document content to validate. Must not be null (the callers always pass a StringBuilder's toString result, which is never null). |

### 4. CRUD Operations / Called Services

| Operation | Target | Details |
|-----------|--------|---------|
| Query | `String.isEmpty()` | Early-exit check for empty strings. |
| Query | `String.length()` | Loop bound for character iteration. |
| Query | `String.charAt(int)` | Access each character sequentially. |
| Query | `Character.isDigit(char)` | Check whether a character is a Unicode digit. |
| Read-only | — | No external state is read or modified. |

### 5. Dependency Trace

| Direction | Method/Class | Notes |
|-----------|-------------|-------|
| Called by | `insertString(FilterBypass, int, String, AttributeSet)` | Validates post-insertion text. |
| Called by | `replace(FilterBypass, int, int, String, AttributeSet)` | Validates post-replacement text. |
| Caller count | 2 methods | Internal to `NumericFilter` only. |

### 6. Per-Branch Detail Blocks

**Block A: Empty string check**

- If `text.isEmpty()` returns `true`, the method returns `true` immediately.
- Rationale: An empty text field is valid (e.g., user deleted everything, or the cursor is at the start).

**Block B: Initialize decimal counter**

- A local variable `decimalCount` of type `byte` is initialized to `0`.
- This counter tracks the number of decimal points encountered so far in the iteration.

**Block C: Character-by-character loop**

- The loop iterates from `i = 0` to `i < text.length()`.
- For each character `ch = text.charAt(i)`:

  - **Sub-branch C1: Character is a decimal point (`.`)**
    - If `decimalCount != 1` (i.e., this is the first dot seen), increment `decimalCount` to `1`.
    - If `decimalCount == 1` (i.e., this is a second dot), the condition is false so no increment occurs; the loop simply continues. This effectively allows the second dot to pass through the `if` block, but it does NOT cause an early return. The loop continues to the next character, which will then be checked by the `else if` clause.
    - Wait — reviewing the logic more carefully: the second dot passes the `if (ch == '.' && decimalCount != 1)` check (condition false), then falls to `else if (!Character.isDigit(ch))`. Since `.` is not a digit, this evaluates to `true` and the method returns `false`. So a second decimal point does cause rejection. The flowchart's "Continue" label was misleading — the second dot actually causes `return false`.

  - **Sub-branch C2: Character is not a decimal point**
    - If `Character.isDigit(ch)` is `false` (e.g., letters, symbols, whitespace, minus sign), return `false` immediately.
    - If `Character.isDigit(ch)` is `true`, continue to the next character.

**Block D: Loop finished — all characters validated**

- If the loop completes without returning `false`, all characters were either digits or the first (and only) decimal point.
- Return `true`.

**Key behavior summary:**

| Input | Output | Reason |
|-------|--------|--------|
| `""` (empty) | `true` | Explicit early return |
| `"123"` | `true` | All digits |
| `"12.34"` | `true` | Digits and one dot |
| `"12.34.56"` | `false` | Second dot triggers `!Character.isDigit('.')` |
| `"-5"` | `false` | Minus sign is not a digit |
| `"12a3"` | `false` | Letter is not a digit |
| `" "` (space) | `false` | Space is not a digit |

---

## Class Dependency Summary

```mermaid
flowchart LR
    Swing["javax.swing.text.DocumentFilter"] --> NumericFilter["NumericFilter"]
    NumericFilter --> isValid["isValid()"]
    insertString["insertString()"] --> isValid
    replace["replace()"] --> isValid
```

`NumericFilter` depends solely on the Swing `DocumentFilter` API for document interception and on its own `isValid()` method for validation logic. It has no external service, database, or configuration dependencies.

## Design Notes

1. **Type choice for `decimalCount`**: The variable uses `byte` instead of `int`. Since the maximum meaningful value is `1` (a second dot causes rejection), an `int` would suffice and is more conventional in Java. The `byte` choice is a minor optimization with no observable behavioral difference.

2. **No negative number support**: The filter rejects minus signs (`-`), plus signs (`+`), and scientific notation (`e`, `E`). It only accepts non-negative numbers with at most one decimal point.

3. **Silent rejection**: Both `insertString` and `replace` silently drop invalid input — no exception, no visual feedback, no logging. Users typing invalid characters will see nothing change, which may feel unresponsive.

4. **Full-document revalidation**: Both methods re-read the entire document content on every keystroke via `doc.getText(0, doc.getLength())`. For typical currency fields (short text), this is negligible. For large documents this would be a performance concern.
