Metadata-Version: 2.4
Name: turkicase
Version: 0.1.1
Summary: Pure-Python Unicode-aware casing for Turkic Latin text
License-Expression: MIT
Keywords: unicode,turkic,turkish,azerbaijani,casing,lowercase
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: twine>=6; extra == "dev"
Dynamic: license-file

# turkicase

`turkicase` is a small, dependency-free Python package for Unicode-aware casing of Turkic Latin text.

It implements the Turkish and Azerbaijani special-casing rules for the two distinct Latin I pairs:

| Uppercase | Lowercase |
| --------- | --------- |
| `I`       | `ı`       |
| `İ`       | `i`       |

Python's built-in casing methods are intentionally locale-independent:

```python
"I".lower()   # "i"
"i".upper()   # "I"
```

For Turkish, Azerbaijani, and other text using Turkic Latin casing rules, the expected mappings are instead:

```text
I ↔ ı
İ ↔ i
```

`turkicase` applies these Turkic-specific mappings while delegating all unrelated Unicode casing behavior to Python.

## Installation

Install the latest release from PyPI:

```bash
python -m pip install turkicase
```

## Quick start

```python
from turkicase import lower, upper, casefold

lower("IĞDIR")       # "ığdır"
lower("İSTANBUL")    # "istanbul"

upper("izmir")       # "İZMİR"
upper("ışık")        # "IŞIK"
```

## Public API

The package provides three primary functions:

```python
lower(text, *, normalization="NFC")
upper(text, *, normalization="NFC")
casefold(text, *, normalization="NFC")
```

The `normalization` argument is keyword-only and accepts:

```
"NFC"
"NFD"
"NFKC"
"NFKD"
None
```

The default is `"NFC"`.

---

## `lower()`

```python
lower(text, *, normalization="NFC")
```

Returns a Turkic-aware lowercase version of `text`.

The core mappings are:

```text
I → ı
İ → i
```

Examples:

```python
from turkicase import lower

lower("I")           # "ı"
lower("İ")           # "i"
lower("IĞDIR")       # "ığdır"
lower("İSTANBUL")    # "istanbul"
```

### Decomposed dotted I

Unicode text does not always represent dotted capital I as the single code point `İ` (`U+0130`).

It may instead use a decomposed sequence:

```text
I + ◌̇
U+0049 + U+0307
```

This sequence must lowercase to a single ordinary `i`:

```python
lower("I\u0307")     # "i"
```

It must not become:

```text
ı + ◌̇
```

The implementation handles this using Unicode's contextual `Before_Dot` and `After_I` rules.

### `Before_Dot`

When processing a capital `I`, the implementation scans forward in the original input for `COMBINING DOT ABOVE` (`U+0307`).

The dot may be reached through intervening combining characters whose canonical combining class is neither `0` nor `230`.

A character with canonical combining class `0` or `230` blocks the context.

Conceptually:

```text
I + ◌̇
→ i
```

But with a blocking character:

```text
I + blocker + ◌̇
→ ı + blocker + ◌̇
```

The target `U+0307` itself is recognized before its combining class is considered as a blocker.

### `After_I`

When processing `U+0307`, the implementation scans backward through the original input.

If the dot belongs contextually to an earlier capital `I`, the dot is deleted during Turkic lowercasing:

```text
I + ◌̇
↓
i
```

A character with canonical combining class `0` or `230` blocks this backward relationship.

### Original input is not normalized first

Casing contexts are evaluated against the original sequence of input code points.

The implementation does not normalize the input before applying `Before_Dot` or `After_I`.

This is important because pre-normalization could compose or rearrange a sequence and hide the contextual relationship that the casing algorithm needs to inspect.

The processing order is therefore:

```text
original input code points
→ evaluate Turkic casing contexts
→ apply casing
→ normalize the result, if requested
```

### Other Unicode lowercase behavior

After the Turkic I rules have been applied, the transformed string is passed to Python's whole-string `str.lower()` implementation.

This preserves Python's normal Unicode behavior for unrelated scripts, including context-sensitive behavior such as Greek final sigma.

---

## `upper()`

```python
upper(text, *, normalization="NFC")
```

Returns a Turkic-aware uppercase version of `text`.

The core mappings are:

```text
i → İ
ı → I
```

Examples:

```python
from turkicase import upper

upper("i")           # "İ"
upper("ı")           # "I"
upper("izmir")       # "İZMİR"
upper("ışık")        # "IŞIK"
```

Internally, ordinary small `i` characters are first changed to capital dotted `İ`.

Python's whole-string `str.upper()` then handles dotless `ı` and all unrelated Unicode uppercase behavior.

---

## `casefold()`

```python
casefold(text, *, normalization="NFC")
```

Returns a Turkic-aware caseless-comparison key.

It first applies Turkic lowercase rules and then applies Python's full Unicode `str.casefold()` operation.

Use `casefold()` for comparison, searching, indexing, and deduplication—not for presentation text.

```python
from turkicase import casefold

casefold("I") == casefold("ı")   # True
casefold("İ") == casefold("i")   # True

casefold("I") == casefold("i")   # False
```

The two Turkic I pairs therefore remain distinct:

```text
I ↔ ı
İ ↔ i
```

Python's full case folding also remains active for unrelated characters:

```python
casefold("Straße")   # "strasse"
```

Because case folding can expand or otherwise transform characters, its result should be treated as a comparison key rather than user-facing text.

---

## Unicode normalization

All three public functions accept a `normalization` argument.

### NFC

```python
normalization="NFC"
```

Canonical decomposition followed by canonical composition.

This is the default and is generally appropriate for normal storage and display text.

```python
upper("i", normalization="NFC")  # "İ" — U+0130
```

### NFD

```python
normalization="NFD"
```

Canonical decomposition without recomposition.

```python
upper("i", normalization="NFD")
# "I\u0307"
# U+0049 + U+0307
```

The NFC and NFD results are canonically equivalent but have different code-point sequences.

### NFKC

```python
normalization="NFKC"
```

Compatibility decomposition followed by canonical composition.

NFKC may change characters unrelated to Turkic casing, including fullwidth forms, ligatures, circled numbers, superscripts, and other compatibility characters.

```python
lower("I①Ａ", normalization="NFKC")
# "ı1a"
```

In this example:

```text
I → ı
① → 1
Ａ → A → a
```

Use NFKC when compatibility distinctions should be folded for matching or normalized input.

Do not use it when preserving the original typographic form is important.

### NFKD

```python
normalization="NFKD"
```

Compatibility decomposition without canonical recomposition.

Like NFKC, it may remove compatibility distinctions, but decomposable characters remain in decomposed form.

### No normalization

```python
normalization=None
```

Skips the final requested normalization step.

```python
result = lower(text, normalization=None)
```

This does not mean the original code-point sequence is preserved unchanged: casing itself may replace, expand, or delete characters.

It only means that no additional `unicodedata.normalize()` operation is requested for the result.

---

## Normalization summary

| Value    |      Compatibility folding | Final form                          |
| -------- | -------------------------: | ----------------------------------- |
| `"NFC"`  |                         No | Canonically composed                |
| `"NFD"`  |                         No | Canonically decomposed              |
| `"NFKC"` |                        Yes | Compatibility-folded and composed   |
| `"NFKD"` |                        Yes | Compatibility-folded and decomposed |
| `None`   | No requested normalization | Casing result as produced           |

For most presentation text, use the default `"NFC"`.

For comparison pipelines that deliberately fold compatibility characters, consider `"NFKC"`.

---

## Longer aliases

Longer descriptive aliases are also exported:

```python
from turkicase import (
    common_turkic_lower,
    common_turkic_upper,
    common_turkic_casefold,
)
```

They are aliases of the shorter functions:

```python
common_turkic_lower is lower
common_turkic_upper is upper
common_turkic_casefold is casefold
```

---

## Scope

`turkicase`:

* always applies Turkic casing semantics;
* does not detect the language or locale automatically;
* specializes the Latin `I`, `İ`, `i`, and `ı` mappings;
* implements contextual handling of decomposed `I + U+0307`;
* delegates unrelated Unicode casing behavior to Python;
* has no runtime dependencies;
* includes typing information through `py.typed`.

Use it when the text is known to require Turkish, Azerbaijani, or compatible Common Turkic Latin casing behavior.

Do not apply Turkic casing indiscriminately to text whose language requires the default Unicode mapping `I ↔ i`.

---

## Development

Install the project and its development dependencies:

```bash
python -m pip install -e ".[dev]"
```

Run the test suite:

```bash
python -m pytest
```

Remove previous build artifacts before producing a new release:

```bash
rm -rf -- dist
rm -rf -- build
rm -rf -- src/turkicase.egg-info
```

Build the source distribution and wheel:

```bash
python -m build
```

Check the generated distributions:

```bash
python -m twine check --strict dist/*
```

## License

MIT
