Packages

A port of ucwidth from C to Elixir, for determining the width (full-width or half-width) of an Unicode character.

Current section

Files

Jump to
ucwidth lib ucwidth.ex
Raw

lib/ucwidth.ex

defmodule Ucwidth do
@moduledoc """
A module to determine the width of a Unicode charactor (or codepoint) on monotyped screens.
A quick comparing between full-width and half-width:
```elixir
"丐" # 1 full-width character
"gg" # 2 half-width characters
```
This module is ported from [Dr Markus Kuhn](https://www.cl.cam.ac.uk/~mgk25/)'s [ucwidth library](https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c) in C.
"""
@max_codepoint 0x10FFFF
# Combining characters take no room for displaying.
# This list is copied from the original C code, which
# is generated by:
#
# ```sh
# uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c
# ```
@combining {
{0x0300, 0x036F},
{0x0483, 0x0486},
{0x0488, 0x0489},
{0x0591, 0x05BD},
{0x05BF, 0x05BF},
{0x05C1, 0x05C2},
{0x05C4, 0x05C5},
{0x05C7, 0x05C7},
{0x0600, 0x0603},
{0x0610, 0x0615},
{0x064B, 0x065E},
{0x0670, 0x0670},
{0x06D6, 0x06E4},
{0x06E7, 0x06E8},
{0x06EA, 0x06ED},
{0x070F, 0x070F},
{0x0711, 0x0711},
{0x0730, 0x074A},
{0x07A6, 0x07B0},
{0x07EB, 0x07F3},
{0x0901, 0x0902},
{0x093C, 0x093C},
{0x0941, 0x0948},
{0x094D, 0x094D},
{0x0951, 0x0954},
{0x0962, 0x0963},
{0x0981, 0x0981},
{0x09BC, 0x09BC},
{0x09C1, 0x09C4},
{0x09CD, 0x09CD},
{0x09E2, 0x09E3},
{0x0A01, 0x0A02},
{0x0A3C, 0x0A3C},
{0x0A41, 0x0A42},
{0x0A47, 0x0A48},
{0x0A4B, 0x0A4D},
{0x0A70, 0x0A71},
{0x0A81, 0x0A82},
{0x0ABC, 0x0ABC},
{0x0AC1, 0x0AC5},
{0x0AC7, 0x0AC8},
{0x0ACD, 0x0ACD},
{0x0AE2, 0x0AE3},
{0x0B01, 0x0B01},
{0x0B3C, 0x0B3C},
{0x0B3F, 0x0B3F},
{0x0B41, 0x0B43},
{0x0B4D, 0x0B4D},
{0x0B56, 0x0B56},
{0x0B82, 0x0B82},
{0x0BC0, 0x0BC0},
{0x0BCD, 0x0BCD},
{0x0C3E, 0x0C40},
{0x0C46, 0x0C48},
{0x0C4A, 0x0C4D},
{0x0C55, 0x0C56},
{0x0CBC, 0x0CBC},
{0x0CBF, 0x0CBF},
{0x0CC6, 0x0CC6},
{0x0CCC, 0x0CCD},
{0x0CE2, 0x0CE3},
{0x0D41, 0x0D43},
{0x0D4D, 0x0D4D},
{0x0DCA, 0x0DCA},
{0x0DD2, 0x0DD4},
{0x0DD6, 0x0DD6},
{0x0E31, 0x0E31},
{0x0E34, 0x0E3A},
{0x0E47, 0x0E4E},
{0x0EB1, 0x0EB1},
{0x0EB4, 0x0EB9},
{0x0EBB, 0x0EBC},
{0x0EC8, 0x0ECD},
{0x0F18, 0x0F19},
{0x0F35, 0x0F35},
{0x0F37, 0x0F37},
{0x0F39, 0x0F39},
{0x0F71, 0x0F7E},
{0x0F80, 0x0F84},
{0x0F86, 0x0F87},
{0x0F90, 0x0F97},
{0x0F99, 0x0FBC},
{0x0FC6, 0x0FC6},
{0x102D, 0x1030},
{0x1032, 0x1032},
{0x1036, 0x1037},
{0x1039, 0x1039},
{0x1058, 0x1059},
{0x1160, 0x11FF},
{0x135F, 0x135F},
{0x1712, 0x1714},
{0x1732, 0x1734},
{0x1752, 0x1753},
{0x1772, 0x1773},
{0x17B4, 0x17B5},
{0x17B7, 0x17BD},
{0x17C6, 0x17C6},
{0x17C9, 0x17D3},
{0x17DD, 0x17DD},
{0x180B, 0x180D},
{0x18A9, 0x18A9},
{0x1920, 0x1922},
{0x1927, 0x1928},
{0x1932, 0x1932},
{0x1939, 0x193B},
{0x1A17, 0x1A18},
{0x1B00, 0x1B03},
{0x1B34, 0x1B34},
{0x1B36, 0x1B3A},
{0x1B3C, 0x1B3C},
{0x1B42, 0x1B42},
{0x1B6B, 0x1B73},
{0x1DC0, 0x1DCA},
{0x1DFE, 0x1DFF},
{0x200B, 0x200F},
{0x202A, 0x202E},
{0x2060, 0x2063},
{0x206A, 0x206F},
{0x20D0, 0x20EF},
{0x302A, 0x302F},
{0x3099, 0x309A},
{0xA806, 0xA806},
{0xA80B, 0xA80B},
{0xA825, 0xA826},
{0xFB1E, 0xFB1E},
{0xFE00, 0xFE0F},
{0xFE20, 0xFE23},
{0xFEFF, 0xFEFF},
{0xFFF9, 0xFFFB},
{0x10A01, 0x10A03},
{0x10A05, 0x10A06},
{0x10A0C, 0x10A0F},
{0x10A38, 0x10A3A},
{0x10A3F, 0x10A3F},
{0x1D167, 0x1D169},
{0x1D173, 0x1D182},
{0x1D185, 0x1D18B},
{0x1D1AA, 0x1D1AD},
{0x1D242, 0x1D244},
{0xE0001, 0xE0001},
{0xE0020, 0xE007F},
{0xE0100, 0xE01EF}
}
@compile {:inline, full_width?: 1}
@doc """
Get width of the code point.
## Parameter
This function accepts following type of paramters:
- an integer within valid unicode code range (`0..0x11ffff`)
- a single character string, e.g `"c"`, `"\\u{3f0a1}"`
and returns the width of the character/codepoint.
## Width definition
* `0` means this character is **invisible** and takes no space on screen.
* `1` means it takes **one cell** to display. For instance, English letters are one cell wide.
* `2` means it takes **two cells** to display. This is quite common in East Asian charsets.
## Examples
iex> Ucwidth.width(0)
0
iex> Ucwidth.width("5")
1
iex> Ucwidth.width("\u303f")
1
iex> Ucwidth.width("\u2329")
2
iex> Ucwidth.width("\u2e80")
2
iex> Ucwidth.width(255)
1
If string length is greater than 1, `{:error, :bad_arg}` will be returned:
iex> Ucwidth.width("abc")
{:error, :bad_arg}
"""
@spec width(non_neg_integer | String.t()) :: 0 | 1 | 2 | {:error, :bad_arg}
def width(one_letter_string_or_integer)
def width(<<code::utf8>>), do: width(code)
def width(0), do: 0
def width(code) when is_integer(code) and code in 0..@max_codepoint do
cond do
combining_char?(code) ->
0
full_width?(code) ->
2
:otherwise ->
1
end
end
def width(_), do: {:error, :bad_arg}
@doc false
def combining_char?(code) do
bin_search(@combining, code, 0, tuple_size(@combining) - 1)
end
defp bin_search(_ranges, _target, min, max) when min > max, do: false
defp bin_search(ranges, target, min, max) do
half = div(min + max, 2)
case elem(ranges, half) do
{a, b} when target >= a and target <= b ->
true
{a, _} when target < a ->
bin_search(ranges, target, min, half - 1)
{_, b} when target > b ->
bin_search(ranges, target, half + 1, max)
end
end
defp full_width?(0x2329), do: true
defp full_width?(0x232A), do: true
defp full_width?(0x303F), do: false
defp full_width?(code) when code in 0x1100..0x115F, do: true
defp full_width?(code) when code in 0x2E80..0xA4CF, do: true
defp full_width?(code) when code in 0xAC00..0xD7A3, do: true
defp full_width?(code) when code in 0xF900..0xFAFF, do: true
defp full_width?(code) when code in 0xFE10..0xFE19, do: true
defp full_width?(code) when code in 0xFE30..0xFE6F, do: true
defp full_width?(code) when code in 0xFF00..0xFF60, do: true
defp full_width?(code) when code in 0xFFE0..0xFFE6, do: true
defp full_width?(code) when code in 0x20000..0x2FFFD, do: true
defp full_width?(code) when code in 0x30000..0x3FFFD, do: true
defp full_width?(_otherwise), do: false
end