Java int to char conversion (without accidental bugs)

I still see integer-to-character bugs ship in production code because the intent is rarely written down. Are you converting a numeric code point (97 → ‘a‘)? Are you converting a digit value (5 → ‘5‘)? Are you converting an arbitrary integer ID into something printable? Those are three different problems, and Java gives you different tools for each.

In this post, I’ll show you the two conversions most people mean by “int to char”: (1) interpreting an int as a Unicode code unit/code point and (2) converting a number into a digit character for some radix (base 10, base 16, etc.). I’ll also cover the sharp edges: narrowing casts, truncation, invalid ranges, surrogate pairs, and why “ASCII math” can quietly give you letters when you expected digits.

By the end, you’ll be able to look at an int and choose a conversion that matches your intent, write it in a way your future self won’t misread, and add validation so weird inputs don’t turn into weird output.

The mental model: what a Java char actually is

In Java, int is a 32-bit signed integer. char is a 16-bit unsigned value that represents a UTF-16 code unit. That last phrase matters.

  • A char is not “a character” in the human sense. It’s one 16-bit chunk of UTF-16.
  • Many common characters fit in one char (Basic Multilingual Plane, U+0000 to U+FFFF).
  • Some characters (like many emoji) need two char values (a surrogate pair). They are one Unicode code point but two UTF-16 code units.

So when you convert an int to char, you’re either:

1) Treating the int as a numeric code unit and narrowing it to 16 bits, or

2) Converting the int into a textual digit representation.

If you don’t decide which one you mean, you’ll write code that compiles, passes casual tests, and fails when inputs drift.

A quick “intent check” I use in code reviews

Whenever I see ((char) something) or (n + ‘0‘), I ask one question:

What does this integer represent?

  • A Unicode code unit (0..65535): “I already know the exact UTF-16 value I want.”
  • A Unicode code point (0..0x10FFFF): “I want the Unicode scalar value, including emoji.”
  • A digit value (0..radix-1): “I’m formatting one digit for a base.”
  • A number (any int): “I want its decimal/hex textual form, which can be multiple characters.”

Most production bugs happen when the code is doing the first thing (narrowing to 16 bits) while the author intended the last thing (string formatting).

Method 1: explicit cast (treat the int as a UTF-16 code unit)

If you have an int that already represents a Unicode value in the BMP (for example 97 for ‘a‘), the simplest approach is an explicit cast.

Key idea: casting from int to char is a narrowing conversion. Java requires an explicit cast because information may be lost.

Here’s a complete runnable example:

public class IntToCharCastDemo {

public static void main(String[] args) {

int codeUnit = 97; // decimal

char ch = (char) codeUnit; // narrowing conversion

System.out.println(ch); // prints: a

}

}

This works because 97 fits in 0..65535, and in Unicode that value maps to ‘a‘.

The part people forget: truncation happens

Casting doesn’t validate. It keeps only the lower 16 bits.

Example:

public class TruncationDemo {

public static void main(String[] args) {

int value = 70000; // bigger than char range

char ch = (char) value; // truncates to lower 16 bits

System.out.println((int) ch); // prints a different number

System.out.println(ch); // prints some unrelated symbol

}

}

If your intent is “convert an int code point safely,” a raw cast is the wrong tool unless you first validate.

What “truncation” means in practice (and why it’s dangerous)

Truncation isn’t random; it’s deterministic. (char) value is essentially value & 0xFFFF.

That creates two common failure modes:

  • Silent corruption: 70000 becomes some other code unit, and you might not notice because logging/output looks “fine” on some platforms.
  • Security/logging surprises: you might accidentally generate control characters (including \0), directionality marks, or other confusing Unicode code units that make logs hard to read.

I’m not saying “never cast.” I’m saying: if a number comes from outside your process, a raw cast is an implicit decision to accept corruption.

A safer cast when you truly mean a single char

If you are sure you want a single UTF-16 code unit and you want to fail loudly for invalid input:

public class SafeCharFromInt {

public static void main(String[] args) {

int codeUnit = 97;

char ch = toBmpCharStrict(codeUnit);

System.out.println(ch);

}

static char toBmpCharStrict(int codeUnit) {

if (codeUnit < Character.MINVALUE || codeUnit > Character.MAXVALUE) {

throw new IllegalArgumentException(

"Value out of char range (0..65535): " + codeUnit);

}

return (char) codeUnit;

}

}

I recommend this style when the integer is coming from outside your process (file, network, database) and you want correctness over “something printable.”

An even stricter version: reject surrogate code units

Sometimes I don’t just want “any UTF-16 code unit,” I want “a single standalone character that is not half of a surrogate pair.” In that case, I reject surrogate ranges.

public class StrictNonSurrogateChar {

static char toNonSurrogateBmpCharStrict(int codeUnit) {

if (codeUnit 0xFFFF) {

throw new IllegalArgumentException("Out of char range: " + codeUnit);

}

char ch = (char) codeUnit;

if (Character.isSurrogate(ch)) {

throw new IllegalArgumentException("Surrogate code unit not allowed: " + codeUnit);

}

return ch;

}

}

Why would I do this? Because if your domain says “one character,” accepting a random surrogate code unit is usually wrong: it can’t be displayed meaningfully on its own.

The “add ‘0‘” trick: when it works, and why it surprises people

You’ll often see this pattern:

char digit = (char) (n + ‘0‘);

This is not general int-to-char conversion. This is digit mapping for base 10, and only for values 0..9.

  • ‘0‘ is the character zero.
  • In Unicode (and ASCII-compatible ranges), ‘0‘ has the numeric value 48.
  • Adding 0..9 yields the code units for ‘0‘..‘9‘.

Here’s the correct use:

public class AddZeroDigitDemo {

public static void main(String[] args) {

int n = 5;

if (n 9) {

throw new IllegalArgumentException("Expected digit 0..9: " + n);

}

char digit = (char) (n + ‘0‘);

System.out.println(digit); // prints: 5

}

}

Why you sometimes get letters instead of digits

If n is not 0..9, you’re just walking forward in the Unicode table.

For example:

  • ‘0‘ is 48
  • 64 + 48 = 112
  • 112 corresponds to ‘p‘

So:

public class AddZeroSurprise {

public static void main(String[] args) {

int n = 64;

char ch = (char) (n + ‘0‘);

System.out.println(ch); // prints: p

}

}

That’s not a bug in Java. It’s a mismatch between intent (“convert number 64 to a character”) and the operation (“offset from the code unit for ‘0‘”).

If you need digits for arbitrary numbers (like 64 → "64"), you want string conversion, not char.

When I still like the ‘0‘ trick

I still use (char) (‘0‘ + digit) in performance-sensitive formatting code, but only when:

  • I’m in a tight loop building a string
  • I have already validated the range
  • The code is obviously about digits (variable name is digit, not n)

I avoid it when the code is likely to be maintained by someone who may not immediately infer the 0..9 constraint. In those cases, Character.forDigit(digit, 10) reads like intent.

Method 2: Character.forDigit (when you mean “digit in a radix”)

When your intent is “turn this numeric value into a single digit character for a given base,” Character.forDigit(int digit, int radix) is usually the cleanest.

  • Works for bases 2 through 36.
  • For digit values 0..radix-1, returns ‘0‘..‘9‘ then ‘a‘..‘z‘.
  • For invalid input, returns the null character \0.

A complete runnable example (base 10):

public class ForDigitBase10 {

public static void main(String[] args) {

int radix = 10;

int digitValue = 5;

char ch = Character.forDigit(digitValue, radix);

if (ch == ‘\\0‘) {

throw new IllegalArgumentException("Invalid digit " + digitValue + " for base " + radix);

}

System.out.println(ch); // prints: 5

}

}

And base 16 (hex):

public class ForDigitHexTable {

public static void main(String[] args) {

int radix = 16;

for (int digitValue = 0; digitValue < radix; digitValue++) {

char ch = Character.forDigit(digitValue, radix);

System.out.print(ch + (digitValue == radix – 1 ? "" : " "));

}

// prints: 0 1 2 3 4 5 6 7 8 9 a b c d e f

}

}

My rule of thumb

  • If you’re formatting numbers for humans (IDs, counts, money), do not force it into a char at all.
  • If you’re writing a low-level encoder/decoder (hex, base32/base36-like formats, custom radices) and you truly need one digit, Character.forDigit is a great fit.

Complementary method: Character.digit (the reverse conversion)

In real systems, you rarely only encode digits; you often decode them too.

Character.digit(char ch, int radix) does the reverse of forDigit for a given base, returning an int digit value (or -1 if invalid). This pairs nicely with validation.

public class DigitRoundTrip {

public static void main(String[] args) {

int radix = 16;

char ch = ‘b‘;

int digit = Character.digit(ch, radix);

if (digit == -1) {

throw new IllegalArgumentException("Not a base-" + radix + " digit: " + ch);

}

char back = Character.forDigit(digit, radix);

System.out.println(digit); // 11

System.out.println(back); // b

}

}

I like this because it makes the “digit vs code unit” distinction explicit in the API name.

Uppercase hex and custom digit alphabets

Character.forDigit returns lowercase letters for digits >= 10. If you need uppercase hex (A..F), you can normalize:

char ch = Character.forDigit(d, 16);

if (ch == ‘\\0‘) throw new IllegalArgumentException();

ch = Character.toUpperCase(ch);

If you need an alphabet beyond 36 digits (or a nonstandard ordering), you’ll need a custom table. That’s one of the few times I’ll hand-roll digit mapping.

Unicode code points: when one “character” needs two chars

If your int represents a Unicode code point, you need to decide whether it fits into one UTF-16 code unit.

  • BMP code points (<= U+FFFF) can be stored in a single char.
  • Supplementary code points (> U+FFFF and <= U+10FFFF) require a surrogate pair (two char values).

If you cast a supplementary code point directly to char, you will truncate and corrupt it.

Here’s the correct way: Character.toChars(int codePoint).

public class CodePointToStringDemo {

public static void main(String[] args) {

int codePoint = 0x1F600; // 😀

if (!Character.isValidCodePoint(codePoint)) {

throw new IllegalArgumentException("Invalid Unicode code point: " + codePoint);

}

String s = new String(Character.toChars(codePoint));

System.out.println(s); // prints: 😀

// If it fits in one char, you can still get it:

if (Character.isBmpCodePoint(codePoint)) {

char ch = (char) codePoint;

System.out.println(ch);

}

}

}

When you actually need a char array

Some APIs want char[] for performance or compatibility:

public class CodePointToCharArray {

public static void main(String[] args) {

int codePoint = 0x1F600;

char[] utf16 = Character.toChars(codePoint);

System.out.println(utf16.length); // 2 for 😀

System.out.println(new String(utf16));

}

}

This distinction becomes important in 2026 because emoji and symbols are everywhere: filenames, chat, UI labels, and test data. If your system ever touches user-generated text, code points above U+FFFF will appear.

Iterating text: chars vs code points

A surprisingly common bug pattern goes like this:

  • Someone loops through a String by char index.
  • Everything works on English test data.
  • Emoji appears and suddenly indexing, truncation, or “one character” assumptions break.

If your logic is about Unicode code points, I prefer String.codePoints():

import java.util.stream.IntStream;

public class CodePointIteration {

public static void main(String[] args) {

String s = "A😀B";

IntStream cps = s.codePoints();

cps.forEach(cp -> System.out.println(Integer.toHexString(cp)));

}

}

This prints code points, not UTF-16 code units. That’s the right level if your int is “a Unicode value.”

Method 3: Integer-to-text conversion (when you want something printable)

A lot of “int to char” requests are actually “int to a printable representation.” If the integer can have more than one digit (like 64, 2026, or -7), the correct output is not a char. It’s a String.

Use:

  • Integer.toString(value) for base 10
  • Integer.toString(value, radix) for another base
  • String.valueOf(value) as a general-purpose conversion

Example:

public class IntToTextDemo {

public static void main(String[] args) {

int value = 64;

System.out.println(Integer.toString(value)); // "64"

System.out.println(Integer.toString(value, 16)); // "40"

}

}

If you try to squeeze this into a char, you’ll end up with one of the earlier categories (code unit or digit), and it will silently be wrong.

When a char return type is a smell

I treat “I need an int converted into a char for logging/UI” as a code smell. It usually means:

  • the domain model is too low-level (should use String), or
  • someone is trying to save allocations prematurely.

If allocations are the real concern, you can still avoid intermediate strings by appending to a StringBuilder:

public class AppendIntToBuilder {

public static void main(String[] args) {

StringBuilder sb = new StringBuilder();

sb.append("value=").append(64);

System.out.println(sb.toString());

}

}

This gives you readable output without inventing a fake “single character” representation.

Choosing the right approach (Traditional vs modern practice)

Most codebases I work on now treat “int to char” as a small design decision: what does the int represent? The modern habit is to make that decision explicit in naming and validation.

Intent

Traditional snippet

Modern practice I recommend —

— Interpret int as a BMP character value

(char) n

Validate range first; name it codeUnit or bmpValue Convert 0..9 to digit

(char) (n + ‘0‘)

Validate 0..9; prefer Character.forDigit(n, 10) when readability matters Convert value to base-N digit

manual math + table

Character.forDigit(digit, radix) with \\0 check Convert Unicode code point to text

cast (breaks for emoji)

new String(Character.toChars(codePoint)) Convert an integer number to text

ad-hoc concatenation

Integer.toString(value) (you get multiple chars)

If you only remember one thing: char is a UTF-16 code unit. If you want “text,” String is almost always the better return type.

A decision tree I actually use

When someone asks me “how do I convert an int to a char in Java?”, I mentally run this checklist:

1) Do you want the textual form of the number?

– Yes → return String (Integer.toString, String.valueOf)

– No → continue

2) Is your int a digit value for a base?

– Yes → Character.forDigit(digit, radix) and validate

– No → continue

3) Is your int a Unicode code point?

– Yes → Character.toChars(codePoint) and produce String or char[]

– No → continue

4) Is your int specifically a UTF-16 code unit?

– Yes → validate 0..65535 (optionally reject surrogates), then cast

– No → you’re missing a requirement; clarify what the number represents

This saves time because it forces the caller (or future reader) to state intent.

Common mistakes I see in reviews (and how I fix them)

Mistake 1: calling something “ASCII” when it’s really Unicode

People often say “ASCII value 97 → ‘a‘.” Java’s char is Unicode/UTF-16, and 97 maps to ‘a‘ because Unicode includes ASCII as a subset. The fix isn’t pedantry; it’s preventing bugs when your team later handles non-English data.

What I write instead:

  • “Unicode code unit value” (when using char)
  • “Unicode code point” (when using int and Character.toChars)

Mistake 2: forgetting that invalid input becomes a valid but wrong char

Casting never fails. It always produces a char, even for -1 or 70000.

I fix it by:

  • validating ranges before casting, or
  • returning Optional when the caller can reasonably recover.

Example:

import java.util.Optional;

public class OptionalChar {

public static void main(String[] args) {

System.out.println(toBmpChar(97));

System.out.println(toBmpChar(70000));

}

static Optional toBmpChar(int codeUnit) {

if (codeUnit 0xFFFF) return Optional.empty();

return Optional.of((char) codeUnit);

}

}

Mistake 3: using forDigit and forgetting it can return \0

Character.forDigit signals invalid input by returning the null character. If you print that, it may look like “nothing happened,” which hides the bug.

My fix is always a guard:

char ch = Character.forDigit(d, radix);

if (ch == ‘\\0‘) {

throw new IllegalArgumentException("Invalid digit");

}

If you want a style that can’t be ignored, wrap it:

public class Digits {

static char digitStrict(int value, int radix) {

char ch = Character.forDigit(value, radix);

if (ch == ‘\\0‘) {

throw new IllegalArgumentException("Digit out of range: " + value + " for base " + radix);

}

return ch;

}

}

Mistake 4: trying to store “a character” in a char when the domain needs code points

If your domain includes arbitrary user text, don’t store it as char at all. Store:

  • a String if you mean “a user-visible character/grapheme-ish thing,” or
  • an int code point if you mean “a Unicode scalar value.”

char is fine for low-level tasks, parsers, and legacy APIs, but it’s rarely the right domain type.

Mistake 5: mixing up “digit value” with “character code”

I see code like:

int n = 9;

char ch = (char) n; // prints a tab, not ‘9‘

Because 9 as a character code is a control character. If you mean the digit ‘9‘, you want:

char ch = Character.forDigit(n, 10);

or

char ch = (char) (n + ‘0‘);

(with validation).

Mistake 6: “one character” vs “one glyph” vs “one user-perceived character”

Even code points aren’t always what users think of as “one character.” Some visible glyphs are formed by multiple code points (for example, combining accents or certain emoji sequences).

If your requirement is “one keypress,” “one visible symbol,” or “truncate to N characters for UI,” this is not an int → char problem. It’s a text-segmentation problem.

In those domains, I avoid char and even avoid “one code point” assumptions unless the requirements explicitly allow it.

Real-world scenarios and edge cases

Scenario: parsing a file format that stores bytes but you need chars

If you’re reading raw bytes (0..255) and want to map them to characters, you almost never want a direct cast to char. You want a charset decoder (like UTF-8, ISO-8859-1, Windows-1252). A cast pretends the byte value is a UTF-16 code unit, which is not decoding.

In modern Java, I reach for:

  • new String(bytes, java.nio.charset.StandardCharsets.UTF_8) (for UTF-8)

If you truly have Latin-1 bytes and want a 1:1 mapping, Java also provides StandardCharsets.ISO88591.

Rule I follow: if you see bytes and text, think “charset,” not casts.

Scenario: generating hex strings for logging or IDs

If you’re building a hex string one digit at a time, Character.forDigit is a clean building block.

public class HexEncoder {

public static void main(String[] args) {

int value = 48879; // 0xBEEF

System.out.println(toHex(value));

}

static String toHex(int value) {

// Unsigned shift keeps leading zeros logic predictable

StringBuilder sb = new StringBuilder(8);

for (int shift = 28; shift >= 0; shift -= 4) {

int digit = (value >>> shift) & 0xF;

sb.append(Character.forDigit(digit, 16));

}

return sb.toString();

}

}

If you don’t need custom formatting, Integer.toHexString(value) is simpler. I only hand-roll this when I need fixed width, separators, or streaming output.

Scenario: formatting unsigned values

Java int is signed, but sometimes you’re holding an unsigned 32-bit value (often from binary protocols). Converting those values to text uses unsigned formatting helpers:

  • Integer.toUnsignedString(value)
  • Integer.toUnsignedString(value, radix)

This is a “number to text” conversion, not an “int to char” conversion, but it’s a common source of confusion. People sometimes try to “fix” negative values with casts, and that’s the wrong tool.

Scenario: working with emoji and symbols

If your data model stores “one character” as a char, it will break on many emoji. In 2026 this isn’t niche; it’s day-to-day.

Instead:

  • store int codePoint if you truly mean one Unicode scalar value, and convert with Character.toChars when rendering.

And if your API requires a String, I typically return a String directly:

public class CodePointText {

static String codePointToStringStrict(int codePoint) {

if (!Character.isValidCodePoint(codePoint)) {

throw new IllegalArgumentException("Invalid code point: " + codePoint);

}

return new String(Character.toChars(codePoint));

}

}

This avoids callers accidentally truncating supplementary characters.

Scenario: parsing a digit character into an int

The reverse of “digit value to digit char” is common in parsers.

If you have a char and want its numeric digit value for a radix:

int d = Character.digit(ch, radix);

if (d == -1) { / invalid / }

Avoid subtracting ‘0‘ unless you truly mean base-10 digits and have validated ch is between ‘0‘ and ‘9‘. That subtraction technique does not handle hex letters, locale-specific digits, or many other valid numeric characters.

Scenario: “numeric value of a character” vs “digit in radix”

Java also has Character.getNumericValue(char) which can return numeric values for a broader set of Unicode characters.

This is useful when:

  • you’re processing arbitrary Unicode text that may contain non-ASCII numerals, and
  • you want a numeric meaning (not just 0..9 in ASCII)

But it’s not a drop-in replacement for Character.digit. For base/radix parsing, Character.digit is clearer and constrained to the radix rules.

Performance considerations (what matters, what usually doesn’t)

These conversions are tiny operations. In real services, the time is usually dominated by I/O, allocations, logging, or JSON parsing.

Still, a few practical notes I’ve learned the hard way:

Avoiding accidental quadratic behavior

The classic performance bug is building text like this in a loop:

String s = "";

for (…) {

s = s + something;

}

That’s not an “int to char” issue, but people often do it when they’re building digit strings.

Use StringBuilder instead. If you’re appending digits one at a time, a builder + Character.forDigit (or ‘0‘ math) is both fast and explicit.

Validation is cheap compared to debugging

I rarely remove checks like if (value 0xFFFF) unless profiling proves it’s a hot path. Most of the time, range validation buys you:

  • better error messages
  • fewer corrupted logs
  • fewer “impossible” production bugs

Prefer clarity when the code is not a hot loop

If you’re not in a tight loop, readability wins. For example, Character.forDigit(d, 10) tells the reader “this is a digit,” which is worth more than micro-optimizing with ‘0‘.

Testing and validation: how I keep int-to-char code from regressing

When the code is simple, a unit test might feel like overkill. But these conversions are exactly where small changes break edge cases.

Here’s how I like to test them:

1) Boundary tests

For BMP code units:

  • 0, 1, 65, 97, 0xD7FF, 0xE000, 0xFFFF
  • reject -1 and 0x10000

For digits:

  • -1, 0, 9, 10 in base 10
  • 0..15 in base 16
  • invalid radices (below 2 or above 36)

For code points:

  • 0, 0x10FFFF valid
  • 0x110000 invalid

2) Round-trip tests

If you encode a digit with forDigit, you should be able to decode it with digit:

  • for each radix 2..36
  • for each digit 0..radix-1

That catches logic drift fast.

3) “Weird input” tests

If you accept input from users or files, include:

  • supplementary code points (emoji)
  • surrogate code units (high/low surrogates as standalone char)
  • values that truncate badly (like 65536, 70000, -1)

Even if you don’t support them, your tests should confirm you reject them cleanly.

What I’d ship in production

When you write “int to char” code, the most valuable improvement is not a different API; it’s making intent explicit.

  • If the int is a Unicode code point: validate with Character.isValidCodePoint(codePoint) and convert with Character.toChars.
  • If the int is a BMP code unit: validate 0..65535 and cast (optionally reject surrogates).
  • If the int is a digit value for a radix: use Character.forDigit and treat \\0 as invalid input.
  • If the int is a normal integer number (64) and you want text: return a String via Integer.toString(value) (or toUnsignedString if needed).

A small “production-ready” helper set

If this logic appears more than once, I consolidate it into helpers so the intent is consistent everywhere:

public class IntCharConversions {

private IntCharConversions() {}

static char bmpCodeUnitStrict(int codeUnit) {

if (codeUnit 0xFFFF) {

throw new IllegalArgumentException("Out of char range: " + codeUnit);

}

return (char) codeUnit;

}

static char nonSurrogateBmpCharStrict(int codeUnit) {

char ch = bmpCodeUnitStrict(codeUnit);

if (Character.isSurrogate(ch)) {

throw new IllegalArgumentException("Surrogate not allowed: " + codeUnit);

}

return ch;

}

static String codePointStrict(int codePoint) {

if (!Character.isValidCodePoint(codePoint)) {

throw new IllegalArgumentException("Invalid code point: " + codePoint);

}

return new String(Character.toChars(codePoint));

}

static char digitStrict(int digitValue, int radix) {

char ch = Character.forDigit(digitValue, radix);

if (ch == ‘\\0‘) {

throw new IllegalArgumentException("Invalid digit " + digitValue + " for base " + radix);

}

return ch;

}

}

This gives me two benefits:

  • callers can’t “accidentally” rely on truncation
  • naming makes the representation explicit (codeUnit, codePoint, digitValue)

Expansion Strategy

Add new sections or deepen existing ones with:

  • Deeper code examples: More complete, real-world implementations
  • Edge cases: What breaks and how to handle it
  • Practical scenarios: When to use vs when NOT to use
  • Performance considerations: Before/after comparisons (use ranges, not exact numbers)
  • Common pitfalls: Mistakes developers make and how to avoid them
  • Alternative approaches: Different ways to solve the same problem

If Relevant to Topic

  • Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
  • Comparison tables for Traditional vs Modern approaches
  • Production considerations: deployment, monitoring, scaling

Final cheat sheet (the one I wish every codebase had)

If you want a one-screen summary:

  • int (code unit 0..65535) → char: validate range, then (char) codeUnit
  • int (code point 0..0x10FFFF) → text: validate with Character.isValidCodePoint, then new String(Character.toChars(cp))
  • int (digit) → char: Character.forDigit(digit, radix) and reject \\0
  • int (number) → text: Integer.toString(value) (or toUnsignedString)

Whenever you’re tempted to write (char) value in production code, add one thing: a variable name or a guard that explains which of those four categories you’re in.

Scroll to Top