
# entropy()

> Shannon entropy over symbols, plus two structural signals that catch the inputs Shannon entropy is blind to.

```ts
import { entropy } from "unsecure/entropy";

entropy("Zk(p4@L!v9{g~8sB");
```

## Signature

```ts
function entropy(data: string | Uint8Array | null | undefined): EntropyResult;

interface EntropyResult {
  // Unigram — standard Shannon, blind to order
  bits: number;
  bitsPerSymbol: number;
  symbolCount: number;
  uniqueSymbols: number;
  maxBitsPerSymbol: number;

  // Bigram — picks up local structure
  bigramBits: number;
  bigramBitsPerSymbol: number;

  // Monotonic runs — catches sorted and reverse-sorted input
  longestRun: number;
  monotonicDirection: "ascending" | "descending" | "none";
}
```

Strings are analysed per Unicode code point, so emoji and CJK count as one symbol each. A `Uint8Array` is analysed per byte value.

## Why three metrics

Shannon entropy counts how often each symbol appears and ignores the order they appear in. That makes it easy to fool:

```ts
entropy("abcdefghijklmnop");
// {
//   bits: 64, bitsPerSymbol: 4, symbolCount: 16, uniqueSymbols: 16,
//   maxBitsPerSymbol: 4,
//   bigramBits: 58.603…, bigramBitsPerSymbol: 3.662…,
//   longestRun: 16, monotonicDirection: "ascending"
// }
```

Four bits per symbol is the maximum for sixteen distinct characters. By the unigram numbers alone, the alphabet in order looks as strong as a random string over the same characters. The other two fields are what give it away:

- **`longestRun` and `monotonicDirection`** catch sorted and reverse-sorted stretches directly, at any input length. Here the whole string is one ascending run.
- **`bigramBits` and `bigramBitsPerSymbol`** measure the distribution of adjacent pairs, which catches sequential, alternating (`ababab`) and blocked (`aaabbbccc`) structure. More powerful than the run detector on long inputs, less useful on short ones.

For comparison, a generated token of the same length:

```ts
entropy("Zk(p4@L!v9{g~8sB");
// bits: 64, bitsPerSymbol: 4, longestRun: 3
```

Same unigram numbers, a much shorter longest run.

## Degenerate and empty input

```ts
entropy("aaaaaaa");
// bits: 0, bitsPerSymbol: 0, uniqueSymbols: 1, longestRun: 1, monotonicDirection: "none"

entropy(""); // every field 0
entropy(null); // every field 0
```

Nullish and empty inputs return zeros rather than throwing.

## Bytes

```ts
entropy(crypto.getRandomValues(new Uint8Array(4096))).bitsPerSymbol;
// ≈ 7.96
```

The ceiling is 8 bits per byte, and how close a random draw gets depends on the sample size, not the source. 256 random bytes score around 7.2, because with 256 draws over 256 values many values never come up at all. Compare against a known-random control of the same length rather than a fixed number.

## Reading `bigramBitsPerSymbol`

The absolute value is length-sensitive. Per-bigram entropy is capped by `log2(min(symbolCount - 1, k²))`, where `k` is `uniqueSymbols`:

- **Short and moderate inputs** (`symbolCount - 1 < k²`) are sample-limited. Random input approaches `log2(symbolCount - 1)`; structure shows up as a value noticeably below that.
- **Very long inputs** (`symbolCount - 1` far above `k²`) are alphabet-limited. Random input approaches `2 * bitsPerSymbol`; structure such as `"abcabc…"` collapses toward `log2(k)`.

Do not compare it against a fixed threshold in isolation. For short inputs, lean on `longestRun` instead.

## Recipe: a password quality gate

```ts
import { entropy } from "unsecure/entropy";

function passesQualityGate(secret: string, minBits = 60): boolean {
  const r = entropy(secret);
  if (r.bits < minBits) return false;
  if (r.longestRun >= 5) return false; // "12345", "abcde"
  return true;
}
```

## Recipe: gate an incoming secret

```ts
import { entropy } from "unsecure/entropy";

function validateSecretStrength(secret: string): boolean {
  const { bitsPerSymbol, uniqueSymbols, longestRun } = entropy(secret);
  return bitsPerSymbol >= 3 && uniqueSymbols >= 10 && longestRun < 4;
}
```

## Pitfall: reading this as cryptographic strength

Shannon entropy measures the information density of the data in front of it, not the security of whatever produced it. `"Qwerty123!@#"` scores well on the unigram fields and is in every cracking dictionary. A run of 16 characters copied out of a random token scores exactly as well as the token.

Use `entropy()` as a heuristic that rejects obvious junk. Do not use it to accept anything, and never use it in place of [`secureGenerate`](/generate/secrets/generate) when you are the one producing the value.
