"""Manipulate 2D strings in Python."""
from functools import reduce, cached_property, lru_cache
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from textwrap import dedent
import uuid
from typing import List, Tuple, Union, Any, Optional
from pandas import DataFrame, Series
from IPython.display import HTML
import numpy as np
import mpmath as mp
[docs]
@dataclass
class BoxParts:
"""Organizes box drawing characters. Provides attribute names that are short and
descriptive. This makes it easier to work with box drawing characters in code.
The tables below show the attribute names, examples, and descriptions of the parts.
.. _box-parts-doc-table-1:
+-------------+-----------+-------------------------------------------+
| Attribute | Example | Description |
| Name | Value | |
+====+===+====+===+===+===+==============+============+===============+
| ul | t | ur | ┌ | ┬ | ┐ | | Upper-left | | Top T | | Upper-right |
| | | | | | | | corner | | cross | | corner |
+----+---+----+---+---+---+--------------+------------+---------------+
| l | c | r | ├ | ┼ | ┤ | | Left T | | Center | | Right T |
| | | | | | | | cross | | cross | | cross |
+----+---+----+---+---+---+--------------+------------+---------------+
| ll | b | lr | └ | ┴ | ┘ | | Lower-left | | Bottom T | | Lower-right |
| | | | | | | | corner | | cross | | corner |
+----+---+----+---+---+---+--------------+------------+---------------+
.. _box-parts-doc-table-2:
+--------------------+----------+------------+
| **Attribute Name** | v | h |
+--------------------+----------+------------+
| **Example Value** | │ | ─ |
+--------------------+----------+------------+
| **Description** | Vertical | Horizontal |
+--------------------+----------+------------+
You can pass any unicode characters to the BoxParts class and it will categorize
them accordingly. But the expectation would be something like the example above.
.. testcode::
from str2d import BoxParts
BoxParts(
v='│', h='─', ul='┌', ur='┐', lr='┘', ll='└',
l='├', r='┤', t='┬', b='┴', c='┼'
)
.. testoutput::
┌─┬─┐
│ │ │
├─┼─┤
│ │ │
└─┴─┘
Or you can pass the any characters you want to the BoxParts class and it will
categorize them accordingly. In this example we use the unicode character for
a full block to be all the box parts.
.. testcode::
BoxParts(*"█"*11)
.. testoutput::
█████
█ █ █
█████
█ █ █
█████
"""
v: str # Vertical line
h: str # Horizontal line
ul: str # Upper-left corner
ur: str # Upper-right corner
lr: str # Lower-right corner
ll: str # Lower-left corner
l: str # Left T (cross)
r: str # Right T (cross)
t: str # Top T (cross)
b: str # Bottom T (cross)
c: str # Center cross
def __str__(self):
"""Return the box parts as a string to illustrate what the box looks like."""
return "\n".join(
[
f"{self.ul}{self.h}{self.t}{self.h}{self.ur}",
f"{self.v} {self.v} {self.v}",
f"{self.l}{self.h}{self.c}{self.h}{self.r}",
f"{self.v} {self.v} {self.v}",
f"{self.ll}{self.h}{self.b}{self.h}{self.lr}",
]
)
def __repr__(self):
"""Return the box parts as a string to illustrate what the box looks like."""
return str(self)
def to_str2d(self):
"""Convert the box parts to a Str2D object."""
return Str2D(data=str(self), halign="left", valign="bottom")
[docs]
class BoxStyle(Enum):
"""Enumerate different box styles. They can be used to draw boxes around text. The
box styles are made up of BoxParts objects that contain the box drawing characters.
The main purpose of this class is to provide a way to apply box drawing over Str2D
objects.
You can see all the box styles by calling the swatches method.
.. testcode::
from str2d import BoxStyle
BoxStyle.swatches()
.. testoutput::
DASHED DASHED
DASHED DOUBLE DOUBLE
SINGLE DASHED DOUBLE LIGHT HEAVY
SINGLE HEAVY DOUBLE HEAVY HEAVY LIGHT
┌─┬─┐ ┏━┳━┓ ┌╌┬╌┐ ┏╍┳╍┓ ┍╍┯╍┑ ┎╌┰╌┒
│ │ │ ┃ ┃ ┃ ╎ ╎ ╎ ╏ ╏ ╏ ╎ ╎ ╎ ╏ ╏ ╏
├─┼─┤ ┣━╋━┫ ├╌┼╌┤ ┣╍╋╍┫ ┝╍┿╍┥ ┠╌╂╌┨
│ │ │ ┃ ┃ ┃ ╎ ╎ ╎ ╏ ╏ ╏ ╎ ╎ ╎ ╏ ╏ ╏
└─┴─┘ ┗━┻━┛ └╌┴╌┘ ┗╍┻╍┛ ┕╍┷╍┙ ┖╌┸╌┚
DASHED DASHED
DASHED TRIPLE TRIPLE DASHED
DASHED TRIPLE LIGHT HEAVY DASHED QUADRUPLE
TRIPLE HEAVY HEAVY LIGHT QUADRUPLE HEAVY
┌┄┬┄┐ ┏┅┳┅┓ ┍┅┯┅┑ ┎┄┰┄┒ ┌┈┬┈┐ ┏┉┳┉┓
┆ ┆ ┆ ┇ ┇ ┇ ┆ ┆ ┆ ┇ ┇ ┇ ┊ ┊ ┊ ┋ ┋ ┋
├┄┼┄┤ ┣┅╋┅┫ ┝┅┿┅┥ ┠┄╂┄┨ ├┈┼┈┤ ┣┉╋┉┫
┆ ┆ ┆ ┇ ┇ ┇ ┆ ┆ ┆ ┇ ┇ ┇ ┊ ┊ ┊ ┋ ┋ ┋
└┄┴┄┘ ┗┅┻┅┛ ┕┅┷┅┙ ┖┄┸┄┚ └┈┴┈┘ ┗┉┻┉┛
DASHED DASHED
QUADRUPLE QUADRUPLE DASHED DASHED DASHED
LIGHT HEAVY SINGLE DOUBLE TRIPLE QUADRUPLE
HEAVY LIGHT ROUND ROUND ROUND ROUND
┍┉┯┉┑ ┎┈┰┈┒ ╭─┬─╮ ╭╌┬╌╮ ╭┄┬┄╮ ╭┈┬┈╮
┊ ┊ ┊ ┋ ┋ ┋ │ │ │ ╎ ╎ ╎ ┆ ┆ ┆ ┊ ┊ ┊
┝┉┿┉┥ ┠┈╂┈┨ ├─┼─┤ ├╌┼╌┤ ├┄┼┄┤ ├┈┼┈┤
┊ ┊ ┊ ┋ ┋ ┋ │ │ │ ╎ ╎ ╎ ┆ ┆ ┆ ┊ ┊ ┊
┕┉┷┉┙ ┖┈┸┈┚ ╰─┴─╯ ╰╌┴╌╯ ╰┄┴┄╯ ╰┈┴┈╯
DOUBLE SINGLE LIGHT HEAVY
DOUBLE SINGLE DOUBLE HEAVY LIGHT
╔═╦═╗ ╓─╥─╖ ╒═╤═╕ ┎─┰─┒ ┍━┯━┑
║ ║ ║ ║ ║ ║ │ │ │ ┃ ┃ ┃ │ │ │
╠═╬═╣ ╟─╫─╢ ╞═╪═╡ ┠─╂─┨ ┝━┿━┥
║ ║ ║ ║ ║ ║ │ │ │ ┃ ┃ ┃ │ │ │
╚═╩═╝ ╙─╨─╜ ╘═╧═╛ ┖─┸─┚ ┕━┷━┙
"""
SINGLE = BoxParts(*"│─┌┐┘└├┤┬┴┼")
SINGLE_HEAVY = BoxParts(*"┃━┏┓┛┗┣┫┳┻╋")
DASHED_DOUBLE = BoxParts(*"╎╌┌┐┘└├┤┬┴┼")
DASHED_DOUBLE_HEAVY = BoxParts(*"╏╍┏┓┛┗┣┫┳┻╋")
DASHED_DOUBLE_LIGHT_HEAVY = BoxParts(*"╎╍┍┑┙┕┝┥┯┷┿")
DASHED_DOUBLE_HEAVY_LIGHT = BoxParts(*"╏╌┎┒┚┖┠┨┰┸╂")
DASHED_TRIPLE = BoxParts(*"┆┄┌┐┘└├┤┬┴┼")
DASHED_TRIPLE_HEAVY = BoxParts(*"┇┅┏┓┛┗┣┫┳┻╋")
DASHED_TRIPLE_LIGHT_HEAVY = BoxParts(*"┆┅┍┑┙┕┝┥┯┷┿")
DASHED_TRIPLE_HEAVY_LIGHT = BoxParts(*"┇┄┎┒┚┖┠┨┰┸╂")
DASHED_QUADRUPLE = BoxParts(*"┊┈┌┐┘└├┤┬┴┼")
DASHED_QUADRUPLE_HEAVY = BoxParts(*"┋┉┏┓┛┗┣┫┳┻╋")
DASHED_QUADRUPLE_LIGHT_HEAVY = BoxParts(*"┊┉┍┑┙┕┝┥┯┷┿")
DASHED_QUADRUPLE_HEAVY_LIGHT = BoxParts(*"┋┈┎┒┚┖┠┨┰┸╂")
SINGLE_ROUND = BoxParts(*"│─╭╮╯╰├┤┬┴┼")
DASHED_DOUBLE_ROUND = BoxParts(*"╎╌╭╮╯╰├┤┬┴┼")
DASHED_TRIPLE_ROUND = BoxParts(*"┆┄╭╮╯╰├┤┬┴┼")
DASHED_QUADRUPLE_ROUND = BoxParts(*"┊┈╭╮╯╰├┤┬┴┼")
DOUBLE = BoxParts(*"║═╔╗╝╚╠╣╦╩╬")
DOUBLE_SINGLE = BoxParts(*"║─╓╖╜╙╟╢╥╨╫")
SINGLE_DOUBLE = BoxParts(*"│═╒╕╛╘╞╡╤╧╪")
LIGHT_HEAVY = BoxParts(*"┃─┎┒┚┖┠┨┰┸╂")
HEAVY_LIGHT = BoxParts(*"│━┍┑┙┕┝┥┯┷┿")
def to_str2d(self):
"""Convert the box style to a Str2D object."""
return "\n".join(self.name.split("_")) / self.value.to_str2d()
def __str__(self):
"""Return the box style as a string to illustrate what the box looks like."""
return str(self.to_str2d())
def __repr__(self):
"""Return the box style as a string to illustrate what the box looks like."""
return str(self)
@classmethod
def swatches(cls):
"""Return a Str2D object with the box style swatches."""
master_box_list = Str2D.equal_width(
*[value.to_str2d() for value in cls.__members__.values()]
)
master_matrix = []
row = None
for i, value in enumerate(master_box_list):
if i % 6 == 0:
row = []
master_matrix.append(row)
row.append(value)
result = Str2D.join_v(
*[Str2D.join_h(*row, sep=" ") for row in master_matrix], sep=Str2D(" ")
)
return result
[docs]
class Str2D:
"""Str2D is a class that allows you to manipulate 2D strings in Python. I had found
myself wanting to paste blocks of text inline with other blocks of text. If you've
ever tried to do this in a text editor, you'll know that it can be a bit of a pain.
vi and vim have a feature called visual block mode that makes this easier. But I
wanted more flexibility and control. So I created Str2D.
Given a multi-line string, Str2D will convert it into something functionally
equivalent where each line separated by a newline character will have the same
number of characters. These are filled in with whatever character is specified but
by default it is a space. This takes care of making sure that each row is the same
width and is primed for manipulation.
The simplest example is to show how to create a Str2D object from a multi-line
string and add it to another Str2D object.
Let's start by constructing a new instance of Str2D and assinging it to the variable
`a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
a
.. testoutput::
a b c d
e f g
h i
j
It might not be clear but the 2nd, 3rd, and 4th lines are padded with spaces to make
each line the same width. We can see this more clearly surrounding the object with
a box.
.. testcode::
a.box()
.. testoutput::
╭───────╮
│a b c d│
│e f g │
│h i │
│j │
╰───────╯
The padded characters are not just spaces. The Str2D object is a structured array
with two fields: 'char' and 'alpha'. The 'char' field is the character array and
the 'alpha' field is a boolean array with a of 1 or 0 for every character in the
character array. The 'alpha' field is used to determine if a character will mask a
second Str2D object when layered on top of it. It is analogous to the alpha channel
in an image where when layered on top of another image, the alpha channel will
indicate which pixels will be visible and which will be transparent. The 'alpha'
field is used in the same way. A value of 1 means the character will be visible and
a value of 0 means the character will be transparent. The padded characters are all
given an 'alpha' value of 0.
If we use the Str3D object to layer another Str2D object below it, we'll see that
the padded characters will be transparent and the characters from the second Str2D
object will be visible.
.. testcode::
from str2d import Str3D
b = Str2D('''\\
.......
.......
.......
.......
''')
Str3D([a, b])
.. testoutput::
a b c d
e f g..
h i....
j......
We can accomplish the same thing by using the `fill_with` method. This method will
fill the padded characters with the specified character. More accurately, it will
replace the characters in the 'char' field where the 'alpha' field is 0 with the
specified character.
.. testcode::
a.fill_with('.')
.. testoutput::
a b c d
e f g..
h i....
j......
If we wanted to make all spaces transparent, we could use the `hide` method. This
will set the 'alpha' field to 0 for all locations where the `char` field matches the
specified character, which defaults to a space.
.. testcode::
a.hide().fill_with('.')
.. testoutput::
a.b.c.d
e.f.g..
h.i....
j......
Let's create a new Str2D object and assign it to the variable `c`. This time we'll
set the `halign` parameter to 'right' which controls how lines with fewer characters
than the maximum width are aligned. By default, it is set to 'left' which will
align the lines to the left as we've seen in the previous example with the variable
`a`.
.. testcode::
c = Str2D('0\\n1 2\\n3 4 5\\n6 7 8 9', halign='right')
c
.. testoutput::
0
1 2
3 4 5
6 7 8 9
We can now see our simple example of adding two Str2D objects together.
.. testcode::
a + c
.. testoutput::
a b c d 0
e f g 1 2
h i 3 4 5
j 6 7 8 9
All of the transparent characters are preserved and we can fill the result as you'd
expect.
.. testcode::
(a + c).fill_with('.')
.. testoutput::
a b c d......0
e f g......1 2
h i......3 4 5
j......6 7 8 9
We'll save the rest for the documentation of the individual methods.
"""
# Structured array data type for Str2D objects
# the 'char' field is the little-endian unicode single character
# the 'alpha' field is an int8 that is trying its best to be a boolean
_dtype = np.dtype([("char", "<U1"), ("alpha", "int8")])
# when doing a transpose, horizontal, or vertical transformations
# shift the alignments to something that makes sense
_align_transpose = {
"top": "left",
"middle": "center",
"bottom": "right",
"left": "top",
"center": "middle",
"right": "bottom",
}
_align_horizontal = {"left": "right", "center": "center", "right": "left"}
_align_vertical = {"top": "bottom", "middle": "middle", "bottom": "top"}
####################################################################
# Construction methods #############################################
####################################################################
[docs]
@classmethod
def validate_fill(
cls, fill: Optional[Union[str, Tuple[str, int]]] = None
) -> Tuple[str, int]:
"""Validate the fill value. The fill value is a tuple containing the fill
character and fill alpha value. The fill character is a single character and
the fill alpha value is 0 or 1. This validator will accept a single character
or a tuple containing a single character and an integer. If a single character
is passed, the fill alpha value will be 0. If a tuple is passed, the fill
character will be the first element and the fill alpha value will be the second.
Parameters
----------
fill : Optional[Union[str, Tuple[str, int]]], optional
The fill value, by default None
Returns
-------
Tuple[str, int]
A tuple containing the fill character and fill alpha value.
Raises
------
ValueError
If the fill value is not a single character or a tuple containing a single
character and an integer or if the fill alpha value is not 0 or 1.
"""
if fill is None:
return " ", 0
if isinstance(fill, str):
fill_char = fill
fill_alpha = 0
elif isinstance(fill, (tuple, list)):
fill_char, fill_alpha = fill
else:
raise ValueError("fill must be a str or tuple.")
if len(fill_char) != 1:
raise ValueError("fill_char must be a single character.")
if not fill_alpha in {0, 1}:
raise ValueError("fill_alpha must be 0 or 1.")
return fill_char, fill_alpha
[docs]
@classmethod
def validate_halign(cls, halign: str) -> str:
"""Validate the horizontal alignment. The horizontal alignment can be 'left',
'center', or 'right'. It will lower case the input and raise a ValueError if
the input is not one of the valid options.
Parameters
----------
halign : str
The horizontal alignment.
Returns
-------
str
The validated horizontal alignment.
Raises
------
ValueError
If the horizontal alignment is not 'left', 'center', or 'right'.
"""
halign = halign.lower()
if halign not in {"left", "center", "right"}:
raise ValueError("halign must be one of 'left', 'center', or 'right'.")
return halign
[docs]
@classmethod
def validate_valign(cls, valign: str) -> str:
"""Validate the vertical alignment. The vertical alignment can be 'top',
'middle', or 'bottom'. It will lower case the input and raise a ValueError if
the input is not one of the valid options.
Parameters
----------
valign : str
The vertical alignment.
Returns
-------
str
The validated vertical alignment.
Raises
------
ValueError
If the vertical alignment is not 'top', 'middle', or 'bottom'.
"""
valign = valign.lower()
if valign not in {"top", "middle", "bottom"}:
raise ValueError("valign must be one of 'top', 'middle', or 'bottom'.")
return valign
[docs]
@classmethod
def struct_array_from_string(cls, string: str, **kwargs) -> "Str2D":
"""Create a structured array from a string. This is likely the most common way
to create an Str2D object. The string is split into lines and then each line is
split into characters. The structured array is created with fields 'char' and
'alpha' where the 'char' field contains the characters and the 'alpha' field
contains 1 for each character.
Though we aren't returning a Str2D object, we are returning a structured array
that potentially needs to be padded and therefore we take the same keyword
arguments as the Str2D constructor.
Parameters
----------
string : str
A string to convert to a structured array.
min_width : int, optional
The minimum width of the output, by default 0.
min_height : int, optional
The minimum height of the output, by default 0.
halign : str, optional
The horizontal alignment, by default 'left'.
valign : str, optional
The vertical alignment, by default 'top'.
fill : Tuple[str, int], optional
The fill value for the 'char' and 'alpha' fields, by default (' ', 0).
Returns
-------
np.ndarray
A structured array with fields 'char' and 'alpha' created from the input
string.
"""
pre_data = [list(row) for row in string.splitlines()]
# needed to use `pop or 0` as opposed to `pop(key, 0)`
# because `None` may have been explicitly passed
min_width = kwargs.pop("min_width", 0) or 0
min_height = kwargs.pop("min_height", 0) or 0
data_width = max(map(len, pre_data), default=0)
data_height = len(pre_data)
width = max(data_width, min_width)
height = max(data_height, min_height)
data = np.empty((height, width), dtype=cls._dtype)
fill = kwargs.pop("fill", (" ", 0))
data.fill(fill)
halign = kwargs.pop("halign", "left")
valign = kwargs.pop("valign", "top")
start_v = 0
if valign == "middle":
start_v = (height - data_height) // 2
elif valign == "bottom":
start_v = height - data_height
for i, row in enumerate(pre_data, start_v):
this_width = len(row)
start = 0
if halign == "center":
start = (width - this_width) // 2
elif halign == "right":
start = width - this_width
for j, char in enumerate(row, start):
data[i, j] = (char, 1)
return data
[docs]
@classmethod
def struct_array_from_char_array(cls, array: np.ndarray) -> "Str2D":
"""Create a structured array from a character array. This is useful when you
have a 2D array of characters and you want to convert it to a structured array
with fields 'char' and 'alpha'.
Parameters
----------
array : np.ndarray
A 2D character array.
Returns
-------
np.ndarray
A structured array with fields 'char' and 'alpha' created from the input
character array.
"""
data = np.empty(array.shape, dtype=cls._dtype)
data["char"] = array
data["alpha"] = 1
return data
[docs]
@classmethod
def struct_array_from_bool_array(cls, array: np.ndarray, char: str) -> "Str2D":
"""Create a structured array from a boolean array. This is useful when you have
a boolean array and you want to convert it to a structured array with fields
'char' and 'alpha'.
The 'char' field will be filled with the specified character where the boolean
array is True and ' ' where the boolean array is False. The 'alpha' field will
be 1 where the boolean array is True and 0 where the boolean array is False.
Parameters
----------
array : np.ndarray
A 2D boolean array.
char : str
The character to use when the boolean array is True.
Returns
-------
np.ndarray
A structured array with fields 'char' and 'alpha' created from the input
boolean array.
"""
data = np.empty(array.shape, dtype=cls._dtype)
data["char"] = np.where(array, char, " ")
data["alpha"] = np.where(array, 1, 0)
return data
[docs]
@classmethod
def parse(cls, data: Optional[Any] = None, **kwargs) -> "Str2D":
"""Parse the input data into a structured array. This is the main method that
will take the input data and convert it into a structured array with fields
'char' and 'alpha'. The input data can be a string, a structured array, a
boolean array, a DataFrame, a Series, or an iterable. If the input data is a
structured array, the keyword arguments will be ignored.
Parameters
----------
data : Optional[Any], optional
The input data, by default None.
kwargs : dict
Additional keyword arguments to pass to the struct_array_from_string method.
If the input data is a structured array, these keyword arguments will be
ignored.
Returns
-------
np.ndarray
A structured array with fields 'char' and 'alpha' created from the input
data.
"""
if isinstance(data, Str2D):
data = data.data
elif isinstance(data, np.ndarray):
if data.dtype == bool:
char = kwargs.pop("char", "█")
data = cls.struct_array_from_bool_array(data, char)
elif data.dtype != cls._dtype:
data = cls.struct_array_from_char_array(data)
else:
data = data.copy()
elif isinstance(data, str):
data = cls.struct_array_from_string(data, **kwargs)
elif isinstance(data, (DataFrame, Series)):
data = cls.struct_array_from_string(data.to_string(), **kwargs)
elif hasattr(data, "__iter__") and not isinstance(data, str):
data = cls.struct_array_from_string("\n".join(map(str, data)), **kwargs)
elif data is not None:
data = cls.struct_array_from_string(str(data), **kwargs)
else:
data = cls.struct_array_from_string("", **kwargs)
return data
[docs]
def __init__(
self,
data: Optional[Any] = None,
min_width: Optional[int] = None,
min_height: Optional[int] = None,
halign: Optional[str] = "left",
valign: Optional[str] = "top",
fill: Optional[Union[str, Tuple[str, int]]] = None,
**kwargs,
) -> None:
"""Create a new Str2D object. The input data can be a string, a structured
array, a boolean array, a DataFrame, a Series, or an iterable. If the input
data is a structured array, the keyword arguments will be ignored.
Parameters
----------
data : Optional[Any], optional
The input data, by default None.
min_width : Optional[int], optional
The minimum width of the output, by default None.
min_height : Optional[int], optional
The minimum height of the output, by default None.
halign : Optional[str], optional
The horizontal alignment, by default 'left'.
valign : Optional[str], optional
The vertical alignment, by default 'top'.
fill : Optional[Union[str, Tuple[str, int]]], optional
The fill value, by default None.
**kwargs : dict
Additional keyword arguments to pass to the parse method.
Raises
------
ValueError
If the fill, horizontal alignment, or vertical alignment value is invalid.
"""
fill = self.validate_fill(fill)
halign = self.validate_halign(halign)
valign = self.validate_valign(valign)
my_kwargs = {
"min_width": min_width,
"min_height": min_height,
"halign": halign,
"valign": valign,
"fill": fill,
}
data = self.parse(data, **my_kwargs, **kwargs)
height_input, width_input = data.shape
height_data = max(height_input, min_height or 0)
width_data = max(width_input, min_width or 0)
left = 0
right = width_data - width_input
if halign == "center":
left = (width_data - width_input) // 2
right = width_data - width_input - left
elif halign == "right":
left = width_data - width_input
right = 0
top = 0
bottom = height_data - height_input
if valign == "middle":
top = (height_data - height_input) // 2
bottom = height_data - height_input - top
elif valign == "bottom":
top = height_data - height_input
bottom = 0
data = self.struct_pad(data, ((top, bottom), (left, right)), fill=fill)
self.data = data
self.halign = halign
self.valign = valign
self.fill = fill
####################################################################
# Math Operations ##################################################
####################################################################
def __bool__(self) -> bool:
"""Return True if the Str2D object has any characters."""
return self.shape != (0, 0)
[docs]
def __add__(self, other: Union["Str2D", str], right_side=False) -> "Str2D":
"""Adding one Str2D object to another or a string to a Str2D object is the main
point of this class. We can add a Str2D object to another Str2D object in order
to concatenate them horizontally. Both objects will be expanded to have the
same height while respecting the alignment parameters. When adding a string to
a Str2D object, the string will be wrapped in a Str2D object and treated as
described above. In the case of adding another Str2D object, the expansion will
use the expansion argument `'mode'` set to `'edge'`. This allows the expansion
to repeat the string intuitively to the edge of the Str2D object.
Parameters
----------
other : Union[Str2D, str]
The Str2D object or string to add.
right_side : bool, optional
If True, the Str2D object will be added to the right side of the
other Str2D object, by default False.
Returns
-------
Str2D
A new Str2D object with the added data.
See Also
--------
add : Add the data.
__add__ : Add the data.
__radd__ : Add the data.
add : Add the data.
Examples
--------
Let's create several instances of Str2D and assign them to the variables `a`,
`b`, and `c`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
b = Str2D('1 2 3\\n4 5 6\\n7 8 9')
c = Str2D('x y z\\nu v w\\nq r s')
We can add the data horizontally by using the Str2D objects with the `+`
operator.
.. testcode::
a + b + c
.. testoutput::
a b c d1 2 3x y z
e f g 4 5 6u v w
h i 7 8 9q r s
j
We can also add a string to the Str2D object.
.. testcode::
a + ' hello ' + c
.. testoutput::
a b c d hello x y z
e f g hello u v w
h i hello q r s
j hello
From the right side.
.. testcode::
'hello ' + a
.. testoutput::
hello a b c d
hello e f g
hello h i
hello j
"""
other_expand_kwargs = {}
if isinstance(other, str):
other_expand_kwargs["mode"] = "edge"
if other == "":
other_expand_kwargs["mode"] = "constant"
other = Str2D(data=other)
height = max(self.height, other.height)
left = self.expand(y=height - self.height)
right = other.expand(y=height - other.height, **other_expand_kwargs)
if right_side:
left, right = right, left
return Str2D(data=np.hstack((left.data, right.data)), **self.kwargs)
[docs]
def __radd__(self, other: "Str2D") -> "Str2D":
"""Dunder method handling the right side of the addition operation."""
return self.__add__(other, right_side=True)
__radd__.__doc__ += __add__.__doc__
[docs]
def add(self, *args, **kwargs) -> "Str2D":
"""Public method for adding the data without the need to use the `+` operator.
We use the `join_h` method to concatenate the data horizontally with additional
arguments.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to add.
kwargs : dict
Additional keyword arguments to pass to the `join_h` method.
Returns
-------
Str2D
A new Str2D object with the added data.
See Also
--------
__add__ : Add the data.
__radd__ : Add the data.
add : Add the data.
"""
return Str2D.join_h(self, *args, **kwargs)
[docs]
def __mul__(self, other: int) -> "Str2D":
"""It doesn't make sense to multiply a Str2D object by another Str2D object.
However, we can multiply a Str2D object by an integer. This will repeat the
data equal to the integer value. The direction of the repetition is determined
by the side of the Str2D object that the integer is on. If the integer is on
the left side, the data will be repeated vertically. If the integer is on the
right side, the data will be repeated horizontally.
Parameters
----------
other : int
The number of times to repeat the data.
Returns
-------
Str2D
A new Str2D object with the repeated data.
See Also
--------
__rmul__ : Multiply the data.
__mul__ : Multiply the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can multiply the data by multiplying by an integer on the right side.
.. testcode::
a * 2
.. testoutput::
a b c da b c d
e f g e f g
h i h i
j j
We can also multiply the data by multiplying by an integer on the left side.
.. testcode::
2 * a
.. testoutput::
a b c d
e f g
h i
j
a b c d
e f g
h i
j
"""
return Str2D(data=np.hstack([self.data] * other), **self.kwargs)
[docs]
def __rmul__(self, other: int) -> "Str2D":
return Str2D(data=np.vstack([self.data] * other), **self.kwargs)
__rmul__.__doc__ = __mul__.__doc__
[docs]
def __truediv__(self, other: Union["Str2D", str], right_side=False) -> "Str2D":
"""Division is the vertical analog of the addition operation. We can divide a
Str2D object by another Str2D object in order to concatenate them vertically.
Both objects will be expanded to have the same width while respecting the
alignment parameters. When dividing by str object, the expansion will use the
expansion argument `'mode'` set to `'edge'`. This allows the expansion to
repeat the string intuitively to the edge of the Str2D object.
Parameters
----------
other : Union[Str2D, str]
The Str2D object or string to divide.
right_side : bool, optional
If True, the Str2D object will be below the other Str2D object, by default
False.
Returns
-------
Str2D
A new Str2D object with the divided data.
See Also
--------
__truediv__ : Divide the data.
__rtruediv__ : Divide the data.
div : Divide the data.
Examples
--------
Let's create several instances of Str2D and assign them to the variables `a`,
`b`, and `c`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
b = Str2D('1 2 3\\n4 5 6\\n7 8 9')
c = Str2D('x y z\\nu v w\\nq r s')
We can divide the data vertically by using the Str2D objects with the `/`
.. testcode::
a / b / c
.. testoutput::
a b c d
e f g
h i
j
1 2 3
4 5 6
7 8 9
x y z
u v w
q r s
We can also divide by a string.
.. testcode::
a / '-' / c
.. testoutput::
a b c d
e f g
h i
j
-------
x y z
u v w
q r s
From the right side.
.. testcode::
'-' / a
.. testoutput::
-------
a b c d
e f g
h i
j
"""
other_expand_kwargs = {}
if isinstance(other, str):
other_expand_kwargs["mode"] = "edge"
if other == "":
other_expand_kwargs["mode"] = "constant"
other = Str2D(data=other)
width = max(self.width, other.width)
left = self.expand(x=width - self.width)
right = other.expand(x=width - other.width, **other_expand_kwargs)
if right_side:
left, right = right, left
return Str2D(data=np.vstack((left.data, right.data)), **self.kwargs)
[docs]
def __rtruediv__(self, other: "Str2D") -> "Str2D":
return self.__truediv__(other, right_side=True)
__rtruediv__.__doc__ = __truediv__.__doc__
[docs]
def div(self, *args, **kwargs) -> "Str2D":
"""Public method for dividing the data without the need to use the `/` operator.
We use the `join_v` method to concatenate the data vertically with additional
arguments.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to divide.
kwargs : dict
Additional keyword arguments to pass to the `join_v` method.
Returns
-------
Str2D
A new Str2D object with the divided data.
See Also
--------
__truediv__ : Divide the data.
__rtruediv__ : Divide the data.
div : Divide the data.
"""
return Str2D.join_v(self, *args, **kwargs)
####################################################################
# Class methods ####################################################
####################################################################
[docs]
@classmethod
def struct_pad(cls, array, *args, **kwargs) -> "Str2D":
"""Pads a structured array with fields 'char' and 'alpha'. The
method is a wrapper around np.pad that pads the 'char' and 'alpha' fields with
the fill value specified in the 'fill' keyword argument.
Parameters
----------
array : np.ndarray
A structured array with fields 'char' and 'alpha'.
mode: str, optional
The padding mode, by default 'constant'. Other options are documented in
np.pad but the only other one that makes sense in a character array context
is 'edge'.
fill : Tuple[str, int], optional
The fill value for the 'char' and 'alpha' fields, by default (' ', 0)
*args : tuple
Positional arguments to np.pad.
**kwargs : dict
Keyword arguments to np.pad.
Returns
-------
np.ndarray
A structured array with fields 'char' and 'alpha' padded with the fill value
specified in the 'fill' keyword argument.
Examples
--------
Let's use `Str2D` to create a structured array from a string and then pad it.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
a.char
.. testoutput::
array([['a', ' ', 'b', ' ', 'c', ' ', 'd'],
['e', ' ', 'f', ' ', 'g', ' ', ' '],
['h', ' ', 'i', ' ', ' ', ' ', ' '],
['j', ' ', ' ', ' ', ' ', ' ', ' ']], dtype='<U1')
Now let's pad the structured array with the fill value '.'. The first argument
is the data. The next argument is the number of padding elements to add to the
beginning and end of each axis.
.. testcode::
Str2D.struct_pad(a.data, 1, fill=('.', 0))
.. testoutput::
array([['.', '.', '.', '.', '.', '.', '.'],
['.', 'a', ' ', 'b', ' ', 'c', 'd'],
['.', 'e', ' ', 'f', ' ', 'g', ' '],
['.', 'h', ' ', 'i', ' ', ' ', ' '],
['.', 'j', ' ', ' ', ' ', ' ', ' '],
['.', '.', '.', '.', '.', '.', '.']], dtype='<U1')
If we want to control the padding on each side of the array, we can pass a tuple
of integers to the second argument. The first integer is the number of padding
elements to add to the beginning of each axis and the second integer is the
number of padding elements to add to the end of each axis.
.. testcode::
Str2D.struct_pad(a.data, (1, 2), fill=('.', 0))
.. testoutput::
array([['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
['.', 'a', ' ', 'b', ' ', 'c', ' ', 'd', '.', '.'],
['.', 'e', ' ', 'f', ' ', 'g', ' ', ' ', '.', '.'],
['.', 'h', ' ', 'i', ' ', ' ', ' ', ' ', '.', '.'],
['.', 'j', ' ', ' ', ' ', ' ', ' ', ' ', '.', '.'],
['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
['.', '.', '.', '.', '.', '.', '.', '.', '.', '.']], dtype='<U1')
However, you can also control the padding on each side of the array by passing
the number of padding elements to add to the beginning and end of each axis as
as tuple of tuples. The first tuple is the number of padding elements to add to
the beginning and end of the first axis and the second tuple is the number of
padding elements to add to the beginning and end of the second axis.
.. testcode::
Str2D.struct_pad(a.data, ((1, 3), (2, 1)), fill=('.', 0))
.. testoutput::
array([['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
['.', '.', 'a', ' ', 'b', ' ', 'c', ' ', 'd', '.'],
['.', '.', 'e', ' ', 'f', ' ', 'g', ' ', ' ', '.'],
['.', '.', 'h', ' ', 'i', ' ', ' ', ' ', ' ', '.'],
['.', '.', 'j', ' ', ' ', ' ', ' ', ' ', ' ', '.'],
['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
['.', '.', '.', '.', '.', '.', '.', '.', '.', '.'],
['.', '.', '.', '.', '.', '.', '.', '.', '.', '.']], dtype='<U1')
Another thing we can do is to pass a different type of padding mode. The
default is 'constant' which will fill the padding elements with the fill value.
However, we can also use 'edge' which will fill the padding elements with the
nearest edge value.
.. testcode::
Str2D.struct_pad(a.data, 1, mode='edge')
.. testoutput::
array([['a', 'a', ' ', 'b', ' ', 'c', ' ', 'd', 'd'],
['a', 'a', ' ', 'b', ' ', 'c', ' ', 'd', 'd'],
['e', 'e', ' ', 'f', ' ', 'g', ' ', ' ', ' '],
['h', 'h', ' ', 'i', ' ', ' ', ' ', ' ', ' '],
['j', 'j', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
['j', 'j', ' ', ' ', ' ', ' ', ' ', ' ', ' ']], dtype='<U1')
"""
# Using `np.pad` on structured arrays is a bit tricky because it doesn't
# handle the structured array fields very well. We need to pad the 'char'
# and 'alpha' fields separately and then recombine them into a structured
# array.
fill = kwargs.pop("fill", (" ", 0))
char_fill, alpha_fill = fill
char_kwargs = kwargs.copy()
char_mode = char_kwargs.setdefault("mode", "constant")
if char_mode == "constant":
char_kwargs["constant_values"] = char_fill
alpha_kwargs = kwargs.copy()
alpha_mode = alpha_kwargs.setdefault("mode", "constant")
if alpha_mode == "constant":
alpha_kwargs["constant_values"] = alpha_fill
char_pad = np.pad(array["char"], *args, **char_kwargs)
alpha_pad = np.pad(array["alpha"], *args, **alpha_kwargs)
padded_data = np.empty(char_pad.shape, dtype=cls._dtype)
padded_data["char"] = char_pad
padded_data["alpha"] = alpha_pad
return padded_data
[docs]
@classmethod
def join_h(cls, *args: "Str2D", sep: str = "") -> "Str2D":
"""`join_h` joins any number of Str2D objects horizontally. The `sep` is the
separator that will be added between the joined data.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to join horizontally.
sep : str, optional
The separator to insert between the joined data, by default ''.
Returns
-------
Str2D
A new Str2D object with the joined data.
See Also
--------
join_v : Join the data vertically
Examples
--------
Let's create several instances of Str2D and assign them to the variables `a`,
`b`, and `c`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
b = Str2D('1 2 3\\n4 5 6\\n7 8 9')
c = Str2D('x y z\\nu v w\\nq r s')
We can join the data horizontally by passing the Str2D objects to the `join_h`
method.
.. testcode::
Str2D.join_h(a, b, c)
.. testoutput::
a b c d1 2 3x y z
e f g 4 5 6u v w
h i 7 8 9q r s
j
We can also pass a separator
.. testcode::
Str2D.join_h(a, b, c, sep='|')
.. testoutput::
a b c d|1 2 3|x y z
e f g |4 5 6|u v w
h i |7 8 9|q r s
j | |
"""
if sep:
args = sum(zip([sep] * len(args), args), ())[1:]
return reduce(lambda x, y: x + y, args)
[docs]
@classmethod
def join_v(cls, *args: "Str2D", sep: str = "") -> "Str2D":
"""`join_v` joins any number of Str2D objects vertically. The `sep` is the
separator that will be added between the joined data.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to join vertically.
sep : str, optional
The separator to insert between the joined data, by default ''.
Returns
-------
Str2D
A new Str2D object with the joined data.
See Also
--------
join_h : Join the data horizontally.
Examples
--------
Let's create several instances of Str2D and assign them to the variables `a`,
`b`, and `c`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
b = Str2D('1 2 3\\n4 5 6\\n7 8 9')
c = Str2D('x y z\\nu v w\\nq r s')
We can join the data vertically by passing the Str2D objects to the `join_v`
.. testcode::
Str2D.join_v(a, b, c)
.. testoutput::
a b c d
e f g
h i
j
1 2 3
4 5 6
7 8 9
x y z
u v w
q r s
We can also pass a separator
.. testcode::
Str2D.join_v(a, b, c, sep='-')
.. testoutput::
a b c d
e f g
h i
j
-------
1 2 3
4 5 6
7 8 9
-------
x y z
u v w
q r s
"""
if sep:
args = sum(zip([sep] * len(args), args), ())[1:]
if args:
return reduce(lambda x, y: x / y, args)
return Str2D()
[docs]
@classmethod
def equal_height(cls, *args: "Str2D") -> List["Str2D"]:
"""Expand each Str2D object to have the same height. Useful for when you want
all Str2D objects to have the same height.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to expand.
Returns
-------
List[Str2D]
A list of Str2D objects with the same height.
"""
max_height = max(arg.height for arg in args)
return [arg.expand(y=max_height - arg.height) for arg in args]
[docs]
@classmethod
def equal_width(cls, *args: "Str2D") -> List["Str2D"]:
"""Expand each Str2D object to have the same width. Useful for when you want
all Str2D objects to have the same width.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to expand.
Returns
-------
List[Str2D]
A list of Str2D objects with the same width.
"""
max_width = max(arg.width for arg in args)
return [arg.expand(x=max_width - arg.width) for arg in args]
[docs]
@classmethod
def equal_shape(cls, *args: "Str2D") -> List["Str2D"]:
"""Expand each Str2D object to have the same shape. Useful for when you want
all Str2D objects to have the same shape.
Parameters
----------
args : Tuple[Str2D]
The Str2D objects to expand.
Returns
-------
List[Str2D]
A list of Str2D objects with the same shape.
"""
max_height = max(arg.height for arg in args)
max_width = max(arg.width for arg in args)
return [
arg.expand(x=max_width - arg.width, y=max_height - arg.height)
for arg in args
]
####################################################################
# Attribute properties #############################################
####################################################################
@property
def height(self) -> int:
"""Return the height of the Str2D object. This is the number of rows in the
structured array or the number of lines in the string."""
return self.data.shape[0]
@property
def width(self) -> int:
"""Return the width of the Str2D object. This is the number of columns in the
structured array or the maximum number of characters in a line in the string."""
return self.data.shape[1]
@property
def shape(self) -> Tuple[int, int]:
"""Return the shape of the Str2D object. This is a tuple containing the height
and width of the structured array."""
return self.data.shape
@property
def char(self) -> np.ndarray:
"""Return the character data. Alternatively, you can access the 'char' field
directly by using the 'data' attribute and the 'char' key.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can access the 'char' field directly by using the 'data' attribute and the
.. testcode::
a.data['char']
.. testoutput::
array([['a', ' ', 'b', ' ', 'c', ' ', 'd'],
['e', ' ', 'f', ' ', 'g', ' ', ' '],
['h', ' ', 'i', ' ', ' ', ' ', ' '],
['j', ' ', ' ', ' ', ' ', ' ', ' ']], dtype='<U1')
This property is a shortcut to the above.
.. testcode::
a.char
.. testoutput::
array([['a', ' ', 'b', ' ', 'c', ' ', 'd'],
['e', ' ', 'f', ' ', 'g', ' ', ' '],
['h', ' ', 'i', ' ', ' ', ' ', ' '],
['j', ' ', ' ', ' ', ' ', ' ', ' ']], dtype='<U1')
"""
return self.data["char"]
@property
def alpha(self) -> np.ndarray:
"""Return the alpha data. Alternatively, you can access the 'alpha' field
directly by using the 'data' attribute and the 'alpha' key.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can access the 'alpha' field directly by using the 'data' attribute and the
.. testcode::
a.data['alpha']
.. testoutput::
array([[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]], dtype=int8)
This property is a shortcut to the above.
.. testcode::
a.alpha
.. testoutput::
array([[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]], dtype=int8)
"""
return self.data["alpha"]
@property
def kwargs(self) -> dict:
"""Return the keyword arguments used to create the Str2D object. This is a
dictionary containing the 'halign', 'valign', and 'fill' values.
The purpose of this property is to make it easier to recreate the Str2D object
with the same settings, helping persist the state of the object as it
transforms.
"""
return {
"halign": self.halign,
"valign": self.valign,
"fill": self.fill,
}
####################################################################
# Transformations ##################################################
####################################################################
TRANSFORMATIONS_DOCSTRING = """
This is one of 5 transformations that can be done to the Str2D object.
- `i` is the identity transformation of the data.
- `t` is the transpose of the data.
- `h` is the horizontal flip of the data.
- `v` is the vertical flip of the data.
- `r` is the 90 degree clockwise rotation of the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('ab\\ncd')
a
.. testoutput::
ab
cd
Notice that this is a 2x2 array. We'll show each of the transformations and
intentionally expand and box them to show how the transformations affect the
alignment.
.. testcode::
a.transormation_palette()
.. testoutput::
i | t | h | v | r | r2 | r3
╭────╮ | ╭────╮ | ╭────╮ | ╭────╮ | ╭────╮ | ╭────╮ | ╭────╮
│ab │ | │ac │ | │ ba│ | │ │ | │ ca│ | │ │ | │ │
│cd │ | │bd │ | │ dc│ | │ │ | │ db│ | │ │ | │ │
│ │ | │ │ | │ │ | │cd │ | │ │ | │ dc│ | │bd │
│ │ | │ │ | │ │ | │ab │ | │ │ | │ ba│ | │ac │
╰────╯ | ╰────╯ | ╰────╯ | ╰────╯ | ╰────╯ | ╰────╯ | ╰────╯
------- | ------- | -------- | --------- | -------- | --------- | ---------
v: top | v: top | v: top | v: bottom | v: top | v: bottom | v: bottom
h: left | h: left | h: right | h: left | h: right | h: right | h: left
Lastly, we can chain the transformations together.
.. testcode::
a.thirvt
.. testoutput::
bd
ac
"""
@cached_property
def t(self) -> "Str2D":
"""Return the transpose of the Str2D object."""
return Str2D(
data=self.data.T,
halign=self._align_transpose[self.valign],
valign=self._align_transpose[self.halign],
fill=self.fill,
)
t.__doc__ += TRANSFORMATIONS_DOCSTRING
@cached_property
def i(self) -> "Str2D":
"""This is the identity transformation. It returns the Str2D object as is."""
return self
i.__doc__ += TRANSFORMATIONS_DOCSTRING
@cached_property
def h(self) -> "Str2D":
"""Return the horizontal flip of the data."""
return Str2D(
data=np.fliplr(self.data),
halign=self._align_horizontal[self.halign],
valign=self.valign,
fill=self.fill,
)
h.__doc__ += TRANSFORMATIONS_DOCSTRING
@cached_property
def v(self) -> "Str2D":
"""Return the vertical flip of the data."""
return Str2D(
data=np.flipud(self.data),
halign=self.halign,
valign=self._align_vertical[self.valign],
fill=self.fill,
)
v.__doc__ += TRANSFORMATIONS_DOCSTRING
@cached_property
def r(self) -> "Str2D":
"""Return the 90 degree rotation of the data."""
return self.t.h
r.__doc__ += TRANSFORMATIONS_DOCSTRING
[docs]
def __getattr__(self, name: str) -> Any:
"""Enables convenient access to the transpose, horizontal flip, vertical flip,
and rotation methods in a concatenated manner.
"""
if (p := name[0].lower()) in ["h", "v", "t", "r", "i"]:
this = getattr(self, p)
if len(name) == 1:
return this
return getattr(getattr(self, p), name[1:])
raise AttributeError(f"'{Str2D.__name__}' object has no attribute '{name}'")
__getattr__.__doc__ += TRANSFORMATIONS_DOCSTRING
####################################################################
# Information/Documentation ########################################
####################################################################
[docs]
def reshape(self, shape: Tuple[int, int]) -> "Str2D":
"""Reshape the Str2D object. This is a wrapper around the `reshape` method of
the structured array.
Parameters
----------
shape : Tuple[int, int]
The new shape of the Str2D object.
Returns
-------
Str2D
A new Str2D object with the reshaped data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
a
.. testoutput::
a b c d
e f g
h i
j
We can reshape the Str2D object to have a shape of (2, 8).
.. testcode::
a.reshape((1, -1))
.. testoutput::
a b c de f g h i j
"""
return Str2D(data=self.data.reshape(shape), **self.kwargs)
[docs]
def show_with_alignment(self, expand=2, box=True) -> "Str2D":
"""Show the alignment parameters with object.
This is useful for debugging and understanding how the alignment parameters
affect the output.
Parameters
----------
expand : int, optional
The amount to expand the Str2D object, by default 2.
box : bool, optional
Whether to box the Str2D object, by default True.
Returns
-------
Str2D
A new Str2D object with the alignment parameters displayed as text.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can show the alignment parameters as part of a new Str2D object.
.. testcode::
a.show_with_alignment()
.. testoutput::
╭─────────╮
│a b c d │
│e f g │
│h i │
│j │
│ │
│ │
╰─────────╯
-----------
v: top
h: left
"""
footer = Str2D(f"v: {self.valign}\nh: {self.halign}")
body = self
if expand:
body = body.expand(expand, expand)
if box:
body = body.box().view
body = Str2D(body, valign="middle", halign="center")
sep = Str2D("-" * max(body.width, footer.width))
return body / sep / footer
[docs]
def transormation_palette(self, sep=" | ", expand=2, box=True) -> "Str2D":
"""Show the transformation palette. This is a visual representation of the
identity, transpose, horizontal flip, vertical flip, and 90 degree rotation of
the data. The transformations are shown with alignment parameters and can be
expanded and boxed.
Parameters
----------
sep : str, optional
The separator between the transformations, by default ' | '.
expand : int, optional
The amount to expand the Str2D object, by default 2.
box : bool, optional
Whether to box the Str2D object, by default True.
See Also
--------
show_with_alignment : Show the alignment parameters as part of a new Str2D
object.
Returns
-------
Str2D
A new Str2D object with the transformations displayed.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('ab\\ncd')
We can show the transformation palette.
.. testcode::
a.transormation_palette()
.. testoutput::
i | t | h | v | r | r2 | r3
╭────╮ | ╭────╮ | ╭────╮ | ╭────╮ | ╭────╮ | ╭────╮ | ╭────╮
│ab │ | │ac │ | │ ba│ | │ │ | │ ca│ | │ │ | │ │
│cd │ | │bd │ | │ dc│ | │ │ | │ db│ | │ │ | │ │
│ │ | │ │ | │ │ | │cd │ | │ │ | │ dc│ | │bd │
│ │ | │ │ | │ │ | │ab │ | │ │ | │ ba│ | │ac │
╰────╯ | ╰────╯ | ╰────╯ | ╰────╯ | ╰────╯ | ╰────╯ | ╰────╯
------- | ------- | -------- | --------- | -------- | --------- | ---------
v: top | v: top | v: top | v: bottom | v: top | v: bottom | v: bottom
h: left | h: left | h: right | h: left | h: right | h: right | h: left
"""
i = self.i.show_with_alignment(expand=expand, box=box)
t = self.t.show_with_alignment(expand=expand, box=box)
h = self.h.show_with_alignment(expand=expand, box=box)
v = self.v.show_with_alignment(expand=expand, box=box)
r = self.r.show_with_alignment(expand=expand, box=box)
rr = self.rr.show_with_alignment(expand=expand, box=box)
rrr = self.rrr.show_with_alignment(expand=expand, box=box)
return self.join_h(
Str2D("i", halign="center") / i,
Str2D("t", halign="center") / t,
Str2D("h", halign="center") / h,
Str2D("v", halign="center") / v,
Str2D("r", halign="center") / r,
Str2D("r2", halign="center") / rr,
Str2D("r3", halign="center") / rrr,
sep=sep,
)
[docs]
def pre(self, **kwargs) -> HTML:
"""Return HTML prefformatted text. This is useful for displaying the Str2D
with different style settings in Jupyter notebooks.
Parameters
----------
font_size : int, optional
The font size, by default 1.
Returns
-------
HTML
The HTML preformatted text.
"""
return pre(self, **kwargs)
####################################################################
# Methods ##########################################################
####################################################################
[docs]
def roll(self, x: int, axis: int) -> "Str2D":
"""Roll the data along an axis. Analogous and wrapper to np.roll.
Parameters
----------
x : int
The number of positions to roll.
axis : int
The axis to roll along.
Returns
-------
Str2D
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can roll the data horizontally by passing the number of positions to roll and
the axis equal to 1.
.. testcode::
a.roll(1, axis=1)
.. testoutput::
da b c
e f g
h i
j
We can roll the data vertically by passing the number of positions to roll and
the axis equal to 0.
.. testcode::
a.roll(1, axis=0)
.. testoutput::
j
a b c d
e f g
h i
"""
return Str2D(
data=np.roll(self.data, x, axis=axis),
halign=self.halign,
valign=self.valign,
fill=self.fill,
)
[docs]
@lru_cache
def roll_h(self, x: int) -> "Str2D":
"""Roll the data horizontally.
Parameters
----------
x : int
The number of positions to roll.
Returns
-------
Str2D
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can roll the data horizontally by passing the number of positions to roll.
.. testcode::
a.roll_h(1)
.. testoutput::
da b c
e f g
h i
j
"""
return self.roll(x, axis=1)
[docs]
@lru_cache
def roll_v(self, x: int) -> "Str2D":
"""Roll the data vertically.
Parameters
----------
x : int
The number of positions to roll.
Returns
-------
Str2D
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can roll the data vertically by passing the number of positions to roll.
.. testcode::
a.roll_v(1)
.. testoutput::
j
a b c d
e f g
h i
"""
return self.roll(x, axis=0)
[docs]
def pad(self, *args, **kwargs) -> "Str2D":
"""`pad` pads the data with the fill value specified in the 'fill' keyword and
is a wrapper around the class method `struct_pad` which in turn is a wrapper
around np.pad.
Parameters
----------
fill : Tuple[str, int], optional
The fill value for the 'char' and 'alpha' fields, by default (' ', 0)
mode: str, optional
The padding mode, by default 'constant'. Other options are documented in
np.pad but the only other one that makes sense in a character array context
is 'edge'.
args : tuple
Positional arguments to np.pad.
kwargs : dict
Keyword arguments to np.pad.
Returns
-------
Str2D
A new Str2D object with the padded data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then pad
it.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
a
.. testoutput::
a b c d
e f g
h i
j
Now let's pad the structured array with the fill value '.'. The first argument
is the number of padding elements to add to the beginning and end of each axis.
.. testcode::
a.pad(1, fill=('.', 0))
.. testoutput::
.........
.a b c d.
.e f g .
.h i .
.j .
.........
If we want to control the padding on each side, we can pass a tuple of integers
to the first argument. The first integer is the number of padding elements to
add to the beginning of each axis and the second integer is the number of
padding elements to add to the end of each axis.
.. testcode::
a.pad((1, 2), fill=('.', 0))
.. testoutput::
..........
.a b c d..
.e f g ..
.h i ..
.j ..
..........
..........
However, you can also control the padding on each side of the array by passing
the number of padding elements to add to the beginning and end of each axis as
as tuple of tuples. The first tuple is the number of padding elements to add to
the beginning and end of the first axis and the second tuple is the number of
padding elements to add to the beginning and end of the second axis.
.. testcode::
a.pad(((1, 3), (2, 1)), fill=('.', 0))
.. testoutput::
..........
..a b c d.
..e f g .
..h i .
..j .
..........
..........
..........
Another thing we can do is to pass a different type of padding mode. The
default is 'constant' which will fill the padding elements with the fill value.
However, we can also use 'edge' which will fill the padding elements with the
nearest edge value.
.. testcode::
a.pad(1, mode='edge')
.. testoutput::
aa b c dd
aa b c dd
ee f g
hh i
jj
jj
"""
kwargs.setdefault("fill", self.fill)
padded_data = self.struct_pad(self.data, *args, **kwargs)
return Str2D(data=padded_data, **self.kwargs)
[docs]
def expand(self, x: int = 0, y: int = 0, **kwargs) -> "Str2D":
"""The `expand` method expands the data by adding padding to the top, bottom,
left, and right of the data. This is a wrapper around the `pad` method where
we take into account the alignment parameters and adjust the padding
accordingly.
Parameters
----------
x : int, optional
The number of columns to expand, by default 0.
y : int, optional
The number of rows to expand, by default 0.
kwargs : dict
Additional keyword arguments to pass to the pad method.
Returns
-------
Str2D
A new Str2D object with the expanded data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
expand it.
.. testcode::
from str2d import Str2D
a = Str2D('a b c d\\ne f g\\nh i\\nj')
We can expand the data by passing the number of columns and rows to expand. In
order to see the expansion, we'll use a fill value of '.'.
.. testcode::
a.expand(1, 1, fill='.')
.. testoutput::
a b c d.
e f g .
h i .
j .
........
Let's see what happens when the alignment is set to 'center' and 'middle'. We
use an `x` value of 4 and a `y` value of 2 to add 4 columns and 2 rows.
.. testcode::
a = Str2D(
'a b c d\\ne f g \\nh i \\nj ',
valign='middle', halign='center'
)
a.expand(4, 2, fill='.')
...........
..a b c d..
..e f g ..
..h i ..
..j ..
...........
"""
kwargs["fill"] = self.validate_fill(kwargs.get("fill", self.fill))
top = 0
left = 0
if self.halign == "center":
left = x // 2
elif self.halign == "right":
left = x
if self.valign == "middle":
top = y // 2
elif self.valign == "bottom":
top = y
right = x - left
bottom = y - top
return self.pad(((top, bottom), (left, right)), **kwargs)
[docs]
def split(self, indices_or_sections, axis=0) -> List["Str2D"]:
"""`split` is analogous to np.split and splits the data along an axis. It'll
return a list of Str2D objects split along the specified axis at the specified
indices.
The `ary` being passed to `np.split` is the structured array data.
Parameters
----------
indices_or_sections : int or 1-D array
If `indices_or_sections` is an integer, N, the array will be divided
into N equal arrays along `axis`. If such a split is not possible,
an error is raised.
If `indices_or_sections` is a 1-D array of sorted integers, the entries
indicate where along `axis` the array is split. For example,
``[2, 3]`` would, for ``axis=0``, result in
- ary[:2]
- ary[2:3]
- ary[3:]
If an index exceeds the dimension of the array along `axis`,
an empty sub-array is returned correspondingly.
axis : int, optional
The axis along which to split, default is 0.
Returns
-------
List[Str2D]
A list of Str2D applied to the split data.
Raises
------
ValueError
If `indices_or_sections` is given as an integer, but
a split does not result in equal division.
See Also
--------
insert : Insert a separator between the split data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
split it.
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
We can split the data into 3 equal parts along the vertical axis. We'll use the
complementary `insert` method to join the split data back together to help
visualize the split.
Two equal parts along the vertical axis.
.. testcode::
a.split(2)
.. testoutput::
[abcdef
ghijkl,
mnopqr
stuvwx]
Using the `insert` method to join the split data back together.
.. testcode::
a.insert(2)
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
3 equal parts along the horizontal axis.
Remember that we're using the `insert` method to show the data. Use the `split`
method to get the split data as a list.
.. testcode::
a.insert(3, axis=1)
.. testoutput::
ab cd ef
gh ij kl
mn op qr
st uv wx
Split the data at first and fifth columns.
.. testcode::
a.insert([1, 5], axis=1)
.. testoutput::
a bcde f
g hijk l
m nopq r
s tuvw x
"""
return [
Str2D(data=divided, **self.kwargs)
for divided in np.split(self.data, indices_or_sections, axis)
]
[docs]
def insert(self, indices_or_sections, axis=0, sep=" "):
"""`insert` is a wrapper around the `split` method and is used to visually
insert a separator between the split data. The `sep` is the separator that will
be added between the split data.
If the `sep` is not an empty string, it will be added between the concatenated
data. If the `sep` is an empty string, the data will be concatenated without any
separation and wll resemble the original data. This would be silly to do but
it's a good way to indicate that the method is doing what it's supposed to do.
The actual intention is to use a separator to visually separate the data.
The `ary` being passed to `np.split` is the structured array data.
Parameters
----------
indices_or_sections : int or 1-D array
If `indices_or_sections` is an integer, N, the array will be divided
into N equal arrays along `axis`. If such a split is not possible,
an error is raised.
If `indices_or_sections` is a 1-D array of sorted integers, the entries
indicate where along `axis` the array is split. For example,
``[2, 3]`` would, for ``axis=0``, result in
- ary[:2]
- ary[2:3]
- ary[3:]
If an index exceeds the dimension of the array along `axis`,
an empty sub-array is returned correspondingly.
axis : int, optional
The axis along which to split, default is 0.
sep : str, optional
The separator to insert between the split data, by default ' '.
Returns
-------
Str2D
A new Str2D object with the split data and separator.
See Also
--------
split : Split the data along an axis.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
Let's `insert` 4 equal parts along the vertical axis with a separator of '-'
followed by an `insert` of 3 equal parts along the horizontal axis with a
separator of '|'.
.. testcode::
a.insert(4, sep='-').insert(3, axis=1, sep='|')
.. testoutput::
ab|cd|ef
--|--|--
gh|ij|kl
--|--|--
mn|op|qr
--|--|--
st|uv|wx
"""
if axis == 0:
return Str2D.join_v(*self.split(indices_or_sections, axis=axis), sep=sep)
return Str2D.join_h(*self.split(indices_or_sections, axis=axis), sep=sep)
[docs]
def __getitem__(
self,
key: Union[int, slice, Tuple[Union[int, slice], ...], List[Union[int, slice]]],
) -> "Str2D":
"""Passing on the indexing to the structured array data. This is a wrapper
around the structured array data's __getitem__ method and returns a new Str2D
object with the sliced data.
Parameters
----------
key : Union[int, slice, Tuple[Union[int, slice], ...], List[Union[int, slice]]
The index or slice to pass to the structured array data.
Returns
-------
Str2D
A new Str2D object with the sliced data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a`.
.. testcode::
from str2d import Str2D
a = Str2D('''\\
012345
123450
234501
345012
''')
a
.. testoutput::
012345
123450
234501
345012
We can slice the data by passing a tuple of slices. The first slice is for the
rows and the second slice is for the columns.
.. testcode::
a[:-2, 1:]
.. testoutput::
12345
23450
"""
if isinstance(key, tuple):
new_key = tuple(([k] if np.isscalar(k) else k) for k in key)
elif np.isscalar(key):
new_key = [key]
else:
new_key = key
return Str2D(data=self.data[new_key], **self.kwargs)
[docs]
def circle(self, radius: float, char: str = "*") -> "Str3D":
"""Create a circle over object. This is a wrapper around the `circle` function
and returns a new Str3D object with the circle over the data.
Parameters
----------
radius : float
The radius of the circle. The minimum of the height and width of the data
is used as a reference for the radius equal to 1.
char : str, optional
The character to use for the circle, by default '*'.
Returns
-------
Str3D
A new Str3D object with the circle over the data.
See Also
--------
hole : Create a hole over the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
.. testcode::
from str2d import Str2D
a = 3 * Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx') * 4
a
.. testoutput::
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
We can create a circle over the data.
.. testcode::
a.circle(1, char=' ')
.. testoutput::
abcde bcdef
ghi ghijklghijkl jkl
m qrmnopqrmnopqrmn r
uvwxstuvwxstuvwxstuv
bcdefabcdefabcdefabcde
hijklghijklghijklghijk
nopqrmnopqrmnopqrmnopq
tuvwxstuvwxstuvwxstuvw
cdefabcdefabcdefabcd
g klghijklghijklgh l
mno mnopqrmnopqr pqr
stuvw tuvwx
"""
c = circle(radius, self.height, self.width, char)
return Str3D([c, self])
[docs]
def hole(self, radius, char="*"):
"""
Create a hole over the object. This is a wrapper around the `hole` function and
returns a new Str3D object with the hole over the data. This effectively hides
the surrounding data and only shows the hole.
Parameters
----------
radius : float
The radius of the hole. The minimum of the height and width of the data is
used as a reference for the radius equal to 1.
char : str, optional
The character to use for the hole, by default '*'.
Returns
-------
Str3D
A new Str3D object with the hole over the data.
See Also
--------
circle : Create a circle over the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
create a hole over it.
.. testcode::
from str2d import Str2D
a = 3 * Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx') * 4
.. testoutput::
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
.. testcode::
a.hole(1, char=' ')
.. testoutput::
bcdefabcde
klghijklghijklgh
opqrmnopqrmnopqrmnop
tuvwxstuvwxstuvwxstuvw
abcdefabcdefabcdefabcdef
ghijklghijklghijklghijkl
mnopqrmnopqrmnopqrmnopqr
stuvwxstuvwxstuvwxstuvwx
bcdefabcdefabcdefabcde
ijklghijklghijklghij
qrmnopqrmnopqrmn
tuvwxstuvw
"""
c = hole(radius, self.height, self.width, char)
return Str3D([c, self])
def box_over(self, style: Union[BoxStyle, str] = BoxStyle.SINGLE_ROUND) -> "Str3D":
"""Create a box over the data. The difference between this and `box_around` is
that this method creates a box over the data and the other method creates a box
around the data. The box that is created will cover the edges of the data.
This is useful when preserving the size is important.
Parameters
----------
style : Union[BoxStyle, str], optional
The style of the box, by default BoxStyle.single_round.
Returns
-------
Str3D
A new Str3D object with the box over the data.
See Also
--------
box_around : Create a box around the data.
box : Create a box around the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
create a box over it.
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
.. testcode::
a.box_over()
.. testoutput::
╭────╮
│hijk│
│nopq│
╰────╯
"""
box = Box([self.height - 2], [self.width - 2], style)
return Str3D([box, self])
def box_around(
self, style: Union[BoxStyle, str] = BoxStyle.SINGLE_ROUND
) -> "Str3D":
"""Create a box around the data. The difference between this and `box_over` is
that this method creates a box around the data and the other method creates a
box over the data. The box that is created will surround the data. This is
useful when preserving the content is important.
Parameters
----------
style : Union[BoxStyle, str], optional
The style of the box, by default BoxStyle.single_round.
Returns
-------
Str3D
A new Str3D object with the box around the data.
See Also
--------
box_over : Create a box over the data.
box : Create a box around the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
create a box around it.
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
.. testcode::
a.box_around()
.. testoutput::
╭──────╮
│abcdef│
│ghijkl│
│mnopqr│
│stuvwx│
╰──────╯
"""
s = self.pad(((1, 1), (1, 1)))
box = Box([self.height], [self.width], style)
return Str3D([box, s])
def box(
self, style: Union[BoxStyle, str] = BoxStyle.SINGLE_ROUND, around: bool = True
) -> "Str3D":
"""Wrapper around `box_over` and `box_around` methods. This method will create
a box around or over the data.
Parameters
----------
style : str, optional
The style of the box, by default "single_round".
around : bool, optional
Whether to create a box around the data or over the data, by default True.
Returns
-------
Str3D
A new Str3D object with the box around or over the data.
See Also
--------
box_over : Create a box over the data.
box_around : Create a box around the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
create a box around it.
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
.. testcode::
a.box()
.. testoutput::
╭──────╮
│abcdef│
│ghijkl│
│mnopqr│
│stuvwx│
╰──────╯
We can also create a box over the data.
.. testcode::
a.box(around=False)
.. testoutput::
╭────╮
│hijk│
│nopq│
╰────╯
"""
if around:
return self.box_around(style)
return self.box_over(style)
[docs]
def pi(self):
"""Replace the data with digits of pi. This is a wrapper around the `pi`
function and returns a new Str2D object with the data replaced with the digits
Returns
-------
Str2D
A new Str2D object with the data replaced with the digits of pi.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
.. testcode::
a.pi()
.. testoutput::
3.1415
926535
897932
384626
"""
data = self.data.copy()
i, j = data["alpha"].nonzero()
old_dps = mp.mp.dps
n = len(i)
mp.mp.dps = n
data["char"][i, j] = [*str(mp.pi)][:n]
mp.mp.dps = old_dps
return Str2D(data=data, **self.kwargs)
[docs]
def e(self):
"""Replace the data with digits of e. This is a wrapper around the `e`
function and returns a new Str2D object with the data replaced with the digits
Returns
-------
Str2D
A new Str2D object with the data replaced with the digits of e.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
.. testcode::
a.e()
.. testoutput::
2.7182
818284
590452
353602
"""
data = self.data.copy()
i, j = data["alpha"].nonzero()
old_dps = mp.mp.dps
n = len(i)
mp.mp.dps = n
data["char"][i, j] = [*str(mp.e)][:n]
mp.mp.dps = old_dps
return Str2D(data=data, **self.kwargs)
[docs]
def phi(self):
"""Replace the data with digits of phi. This is a wrapper around the `phi`
function and returns a new Str2D object with the data replaced with the digits
Returns
-------
Str2D
A new Str2D object with the data replaced with the digits of phi.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
.. testcode::
a.phi()
.. testoutput::
1.6180
339887
498948
482045
"""
data = self.data.copy()
i, j = data["alpha"].nonzero()
old_dps = mp.mp.dps
n = len(i)
mp.mp.dps = n
data["char"][i, j] = [*str(mp.phi)][:n]
mp.mp.dps = old_dps
return Str2D(data=data, **self.kwargs)
[docs]
def hide(self, char=" "):
"""Hide where character array is char. This sets the alpha array to 0 where the
character array is equal to char.
Parameters
----------
char : str, optional
The character to hide, by default ' '.
Returns
-------
Str2D
A new Str2D object with the data hidden.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
hide the data.
.. testcode::
from str2d import Str2D
a = Str2D('abcdef\\nghijkl\\nmnopqr\\nstuvwx')
a
.. testoutput::
abcdef
ghijkl
mnopqr
stuvwx
The hiddent data doesn't replace the character with a space but sets the alpha
array to 0 where the character array is equal to the character to hide. So, in
a singular context, we can still see the character and must fill the 0s with a
character to hide the data.
.. testcode::
a.hide(char='m').fill_with(char=' ')
.. testoutput::
abcdef
ghijkl
mno qr
stuvwx
"""
data = self.data.copy()
i, j = (data["char"] == char).nonzero()
data["alpha"][i, j] = 0
return Str2D(data=data, **self.kwargs)
[docs]
def fill_with(self, char=" ") -> "Str2D":
"""Fill the data with the character. This is a wrapper around the `fill` method
from the structured array data.
Parameters
----------
char : str, optional
The character to fill with, by default ' '.
Returns
-------
Str2D
A new Str2D object with the data filled with the character.
See Also
--------
hide : Hide the data.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
fill.
.. testcode::
from str2d import Str2D
a = Str2D('abcdefg\\nhijklm\\nnopqr\\nstuv\\nwxy\\nz')
a
.. testoutput::
abcdefg
hijklm
nopqr
stuv
wxy
z
We can fill the data with a character.
.. testcode::
a.fill_with(char='.')
abcdefg
hijklm.
nopqr..
stuv...
wxy....
z......
"""
kwargs = self.kwargs
kwargs["fill"] = (char, 0)
data = np.where(self.data["alpha"], self.data["char"], char)
return Str2D(data=data, **kwargs)
def __str__(self) -> str:
"""Return the string representation of the data."""
return "\n".join(["".join(row["char"]) for row in self.data])
def __repr__(self) -> str:
"""Return the string representation of the data."""
return str(self)
####################################################################
# String operations whose result is a new Str2D ####################
####################################################################
[docs]
def lower(self) -> "Str2D":
"""Return the lowercase version of the data."""
data = self.data.copy()
data["char"] = np.char.lower(data["char"])
return Str2D(data=data, **self.kwargs)
[docs]
def upper(self) -> "Str2D":
"""Return the uppercase version of the data."""
data = self.data.copy()
data["char"] = np.char.upper(data["char"])
return Str2D(data=data, **self.kwargs)
[docs]
def replace(self, old: str, new: str) -> "Str2D":
"""Replace the data.
Parameters
----------
old : str
The string to replace.
new : str
The string to replace with.
Returns
-------
Str2D
A new Str2D object with the data replaced.
Raises
------
ValueError
If the length of the old and new strings are not the same.
"""
if len(old) != len(new):
raise ValueError("old and new must have the same length.")
data = self.data.copy()
data["char"] = np.char.replace(data["char"], old, new)
return Str2D(data=data, **self.kwargs)
[docs]
def title(self) -> "Str2D":
"""Return the title version of the data."""
data = self.data.copy()
char = "".join(data["char"].ravel()).title()
data["char"] = np.array(list(char)).reshape(data["char"].shape)
return Str2D(data=data, **self.kwargs)
[docs]
def strip(self, *args, **kwargs) -> "Str2D":
"""Strip line by line. Return the stripped version of the data. This is a
wrapper around the `strip` method from the str class."""
data = self.data.copy()
char = ["".join(row).strip(*args, **kwargs) for row in data["char"]]
return Str2D(data=char, **self.kwargs)
[docs]
def lstrip(self, *args, **kwargs) -> "Str2D":
"""Left strip line by line. Return the left stripped version of the data. This
is a wrapper around the `lstrip` method from the str class."""
data = self.data.copy()
char = ["".join(row).lstrip(*args, **kwargs) for row in data["char"]]
return Str2D(data=char, **self.kwargs)
[docs]
def rstrip(self, *args, **kwargs) -> "Str2D":
"""Right strip line by line. Return the right stripped version of the data.
This is a wrapper around the `rstrip` method from the str class."""
data = self.data.copy()
char = ["".join(row).rstrip(*args, **kwargs) for row in data["char"]]
return Str2D(data=char, **self.kwargs)
####################################################################
# String operations whose result is a new boolean array ############
####################################################################
[docs]
def isdigit(self) -> "Str2D":
"""Return whether the data is a digit."""
data = self.data.copy()
data["alpha"] = np.char.isdigit(data["char"]).astype("int8")
return Str2D(data=data, **self.kwargs)
def __eq__(self, other: Any) -> "Str2D":
"""Return whether the data is equal to the other data."""
if isinstance(other, str):
if len(other) == 1:
return self.data["char"] == other
return str(self) == other
if isinstance(other, Str2D):
return self.data["char"] == other.data["char"]
raise ValueError("other must be a Str2D object or a scalar.")
def __ne__(self, other: Any) -> "Str2D":
"""Return whether the data is not equal to the other data."""
eq = self == other
if isinstance(eq, bool):
return not eq
return ~eq
[docs]
def strip2d(self, char=" ") -> "Str2D":
"""Strip the data 2-dimensionally. This method checks how much it can trim from
each side of the data before it reaches a non-matching character. It then trims
the data accordingly.
Parameters
----------
char : str, optional
The character to strip, by default ' '. This is the character that will be
trimmed from the data.
Returns
-------
Str2D
A new Str2D object with the data stripped.
Examples
--------
Let's create an instance of Str2D and assign it to the variable `a` and then
strip the data.
.. testcode::
from str2d import Str2D
a = Str2D('''\\
............
...xxxxxxx..
...xxxxxxx..
...xxxxxxx..
............
............
''')
Striping '.' from the data will leave just the core of 'x's.
.. testoutput::
a.strip2d(char='.')
.. testoutput::
xxxxxxx
xxxxxxx
xxxxxxx
"""
mask = self != char
x0, x1 = np.flatnonzero(mask.any(axis=1))[[0, -1]]
y0, y1 = np.flatnonzero(mask.any(axis=0))[[0, -1]]
return self[x0 : x1 + 1, y0 : y1 + 1]
class Box(Str2D):
"""Subclass of Str2D that is a Box or Window.
I wanted to be able to make window pains with arbitrary number of rows and columns.
I had to override the transformation methods `i`, `t`, `h`, `v`, and `r` because I
wanted to preserve the box characters orientation.
Parameters
----------
spec_v : List[int], optional
The vertical specification, by default None. This is a list of integers that
represent the height of the rows.
spec_h : List[int], optional
The horizontal specification, by default None. This is a list of integers that
represent the width of the columns.
style : Union[BoxStyle, str], optional
The style of the box, by default BoxStyle.SINGLE_ROUND.
Examples
--------
Let's create a box with a single round style.
.. testcode::
from str2d import Box
Box(spec_v=[1, 2, 3], spec_h=[1, 2, 3])
.. testoutput::
╭─┬──┬───╮
│ │ │ │
├─┼──┼───┤
│ │ │ │
│ │ │ │
├─┼──┼───┤
│ │ │ │
│ │ │ │
│ │ │ │
╰─┴──┴───╯
"""
@staticmethod
def spec_to_positions(spec):
"""Convert a specification to positions."""
n = len(spec)
a = np.arange(n) + 1
b = np.add.accumulate(spec)
return np.concatenate(([0], a + b))
@staticmethod
def spec_to_len(spec):
"""Convert a specification to length."""
return sum(spec) + len(spec) + 1
def __init__(
self,
spec_v: Optional[List[int]] = None,
spec_h: Optional[List[int]] = None,
style: Union[BoxStyle, str] = BoxStyle.SINGLE_ROUND,
) -> "Box":
"""Create a box around the data."""
spec_v = [0] if spec_v is None else spec_v
spec_h = [0] if spec_h is None else spec_h
pos_v = self.spec_to_positions(spec_v)
pos_h = self.spec_to_positions(spec_h)
height = self.spec_to_len(spec_v)
width = self.spec_to_len(spec_h)
if isinstance(style, str):
style = BoxStyle[style.upper()]
chars = style.value
self.spec_v = spec_v
self.spec_h = spec_h
self.style = style
self.pos_v = pos_v
self.pos_h = pos_h
self.chars = chars
data = np.empty((height, width), dtype=Str2D._dtype)
data.fill((" ", 0))
super().__init__(data)
self.assign_box_char()
def assign_box_char(self):
"""Assign the box characters."""
data = self.data
pos_v = self.pos_v
pos_h = self.pos_h
chars = self.chars
data[:, pos_h] = (chars.v, 1)
data[pos_v, :] = (chars.h, 1)
data[0, pos_h] = (chars.t, 1)
data[-1, pos_h] = (chars.b, 1)
data[pos_v, 0] = (chars.l, 1)
data[pos_v, -1] = (chars.r, 1)
data[np.ix_(pos_v[1:-1], pos_h[1:-1])] = (chars.c, 1)
pos_v_corners = [[0, 0], [-1, -1]]
pos_h_corners = [[0, -1], [0, -1]]
corners = [[(chars.ul, 1), (chars.ur, 1)], [(chars.ll, 1), (chars.lr, 1)]]
data[pos_v_corners, pos_h_corners] = corners
return self
@cached_property
def t(self):
"""Return the transpose of the data."""
return Box(spec_v=self.spec_h, spec_h=self.spec_v, style=self.style)
@cached_property
def h(self):
"""Return the horizontal flip of the data."""
return Box(spec_v=self.spec_v, spec_h=self.spec_h[::-1], style=self.style)
@cached_property
def v(self):
"""Return the vertical flip of the data."""
return Box(spec_v=self.spec_v[::-1], spec_h=self.spec_h, style=self.style)
@cached_property
def r(self):
"""Return the 90 degree rotation of the data."""
return self.t.h
class Str3D:
"""Manipulate 3D strings in Python."""
def __init__(self, data):
"""Create a Str3D object."""
self.source = data
self.data = np.stack([datum.data for datum in data])
def update(self):
"""Update the data."""
self.data = np.stack([datum.data for datum in self.source])
def __str__(self):
"""Return the string representation of the data."""
return str(self.view)
def __repr__(self):
"""Return the string representation of the data."""
return str(self)
@cached_property
def t(self):
"""Return the transpose of the data."""
return Str3D([datum.t for datum in self.source])
@cached_property
def h(self):
"""Return the horizontal flip of the data."""
return Str3D([datum.h for datum in self.source])
@cached_property
def v(self):
"""Return the vertical flip of the data."""
return Str3D([datum.v for datum in self.source])
@cached_property
def r(self):
"""Return the 90 degree rotation of the data."""
return self.t.h
@lru_cache
def roll(self, *args, **kwargs) -> "Str3D":
"""Roll the data along an axis."""
layer = kwargs.pop("layer", None)
if layer is None:
return Str3D([datum.roll(*args, **kwargs) for datum in self.source])
if np.isscalar(layer):
layer = [layer]
return Str3D(
[
datum if i not in layer else datum.roll(*args, **kwargs)
for i, datum in enumerate(self.source)
]
)
@property
def view(self):
"""Return the view of the data."""
argmax = self.data["alpha"].argmax(axis=0)
_, height, width = self.data.shape
y_indices, x_indices = np.ogrid[:height, :width]
result = self.data[argmax, y_indices, x_indices]
return Str2D(result)
def __getattr__(self, name: str) -> Any:
"""Enables convenient access to the transpose, horizontal flip, vertical flip,
and rotation methods in a concatenated manner.
"""
if hasattr(self.view, name):
return getattr(self.view, name)
raise AttributeError(f"'{Str3D.__name__}' object has no attribute '{name}'")
[docs]
def space(mn: float, mx: float, w: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Return left, center, and right points of a space.
Imagine an integer number of cells. I want to assign the left side of the left most
cell to be `mn` and the right side of the right most cell to be `mx`. This function
returns 3 arrays. The first array consists of the left side of each cell. The
second array consists of the center of each cell. The third array consists of the
right side of each cell.
Let's imagine `w=5` and `mn=0` and `mx=1`. The space is divided into 5 cells.
.. testoutput::
mn=0 0.2 0.4 0.6 0.8 mx=1
│ │ │ │ │ │
╭───────────┬───────────┬───────────┬───────────┬───────────╮
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
├─────┬─────┼─────┬─────┼─────┬─────┼─────┬─────┼─────┬─────┤
left 0.0 │ 0.2 │ 0.4 │ 0.6 │ 0.8 │ │
center 0.1 │ 0.3 │ 0.5 │ 0.7 │ 0.9 │
right 0.2 0.4 0.6 0.8 1.0
We can use these arrays to more accurately model x, y coordinates for ascii plots.
Parameters
----------
mn : float
The minimum value of the space.
mx : float
The maximum value of the space.
w : int
The number of cells.
Returns
-------
Tuple[np.ndarray, np.ndarray, np.ndarray]
The left, center, and right points of the space.
Raises
------
ValueError
If `w` is less than 1.
"""
if w < 1:
raise ValueError(f"{w=} must be >= 1")
a = np.linspace(mn, mx, w + 1)
return a[:-1], (a[:-1] + a[1:]) / 2, a[1:]
[docs]
def region(func, height, width, x_range, y_range):
"""Create a mask where func is True.
Imagine a 2D space that is subdivided into `height` rows and `width` columns. The
edges of the space are defined by `x_range` and `y_range`. This function returns a
mask where `func` is True for the center of each cell.
Parameters
----------
func : Callable[[np.ndarray, np.ndarray], np.ndarray]
The function that returns True if the point is in the region.
height : int
The number of rows.
width : int
The number of columns.
x_range : Tuple[float, float]
The range of the x values.
y_range : Tuple[float, float]
The range of the y values.
Returns
-------
np.ndarray
A mask where `func` is True for the center of each cell.
See Also
--------
boundary : Create a mask where func is True and False.
Examples
--------
Let's imagine `height=10` and `width=20` and `x_range=[0, 1]` and `y_range=[0, 1]`.
Now we'll assume a simple function that returns True if the 1 less the x coordinate
is less than the y coordinate.
... testinput::
height = 10
width = 20
x_range = [0, 1]
y_range = [0, 1]
def func(x, y):
return 1 - x < y
mask = region(func, height, width, x_range, y_range)
Str2D(mask, char='*')
.. testoutput::
*******************
*****************
***************
*************
***********
*********
*******
*****
***
*
"""
_, center, _ = space(*x_range, width)
_, middle, _ = space(*y_range[::-1], height)
return func(center.reshape(1, -1), middle.reshape(-1, 1))
[docs]
def boundary(func, height, width, x_range, y_range):
"""Create a mask where func is True and False.
Imagine a 2D space that is subdivided into `height` rows and `width` columns. The
edges of the space are defined by `x_range` and `y_range`. The mask being returned
is a proxy for each of the cells. The truth of each cell is determined by whether
`func` is True for at least one of the corners of the cell while also being False
for at least one of the corners of the cell. That implies that the function crosses
from being True to False or False to True within the cell and thus a boundary.
Parameters
----------
func : Callable[[np.ndarray, np.ndarray], np.ndarray]
The function that returns True if the point is in the region.
height : int
The number of rows.
width : int
The number of columns.
x_range : Tuple[float, float]
The range of the x values.
y_range : Tuple[float, float]
The range of the y values.
Returns
-------
np.ndarray
A mask where `func` is True for the center of each cell.
See Also
--------
region : Create a mask where func is True.
Examples
--------
Let's imagine `height=10` and `width=20` and `x_range=[0, 1]` and `y_range=[0, 1]`.
Now we'll assume a simple function that returns True if the 1 less the x coordinate
is less than the y coordinate.
... testinput::
height = 10
width = 20
x_range = [0, 1]
y_range = [0, 1]
def func(x, y):
return 1 - x < y
mask = boundary(func, height, width, x_range, y_range)
Str2D(mask, char='*')
.. testoutput::
***
***
***
***
***
***
***
***
***
**
"""
left, _, right = space(*x_range, width)
bottom, _, top = space(*y_range[::-1], height)
mask = np.stack(
[
func(left[None, :], top[:, None]),
func(right[None, :], top[:, None]),
func(right[None, :], bottom[:, None]),
func(left[None, :], bottom[:, None]),
]
)
return mask.any(axis=0) & ~mask.all(axis=0)
[docs]
def circle(radius, height, width, char="*"):
"""Create a circle.
This is a special application of the `boundary` function. The function that
determines the circle is `lambda x, y: x**2 + y**2 < radius**2`.
Parameters
----------
radius : float
The radius of the circle.
height : int
The number of rows.
width : int
The number of columns.
char : str, optional
The character to use, by default '*'.
Returns
-------
Str2D
A new Str2D object with the circle.
See Also
--------
boundary : Create a mask where func is True and False.
Str2D.circle : Create a circle.
"""
height_limited = True
if width < height * 2:
height_limited = False
if height_limited:
y_range = np.array([-1, 1])
x_range = width / height / 2 * y_range
else:
x_range = np.array([-1, 1])
y_range = height * 2 / width * x_range
mask = boundary(
lambda x, y: x**2 + y**2 < radius**2, height, width, x_range, y_range
)
return Str2D(mask, char=char)
[docs]
def hole(radius, height, width, char="*"):
"""Create a hole.
This is a special application of the `region` function. The function that
determines the hole is `lambda x, y: x**2 + y**2 <= radius**2`.
Parameters
----------
radius : float
The radius of the hole.
height : int
The number of rows.
width : int
The number of columns.
char : str, optional
The character to use, by default '*'.
Returns
-------
Str2D
A new Str2D object with the hole.
See Also
--------
region : Create a mask where func is True.
Str2D.hole : Create a hole.
"""
height_limited = True
if width < height * 2:
height_limited = False
if height_limited:
y_range = np.array([-1, 1])
x_range = width / height / 2 * y_range
else:
x_range = np.array([-1, 1])
y_range = height * 2 / width * x_range
mask = region(
lambda x, y: x**2 + y**2 <= radius**2, height, width, x_range, y_range
)
return Str2D(~mask, char=char)
def is_in_mandelbrot(
x: float, y: float, max_iterations: int = 25
) -> Union[bool, np.ndarray]:
"""
Determines if the point (x, y) is in the Mandelbrot set.
Parameters
----------
x : float
The x-coordinate of the point.
y : float
The y-coordinate of the point.
max_iterations : int, optional
The maximum number of iterations, by default 25.
Returns
-------
Union[bool, np.ndarray]
True if the point is in the Mandelbrot set, False otherwise.
"""
if np.isscalar(x):
x = np.array([x])
if np.isscalar(y):
y = np.array([y])
n = len(y.ravel())
m = len(x.ravel())
c = np.empty((n, m), dtype=np.complex128)
c.real = x.reshape(1, -1)
c.imag = y.reshape(-1, 1)
z = np.zeros((n, m), dtype=np.complex128)
absz = abs(z)
thresh = 10
for _ in range(max_iterations):
z = np.where(absz < thresh, z * z + c, thresh)
absz = abs(z)
return absz < 2
[docs]
def mandelbrot(height, width, x_range, y_range, char="*"):
"""Create a Mandelbrot set.
Visualize the Mandelbrot set. The Mandelbrot set as a Str2d object.
Parameters
----------
height : int
The number of rows.
width : int
The number of columns.
x_range : Tuple[float, float]
The range of the x values.
y_range : Tuple[float, float]
The range of the y values.
char : str, optional
The character to use, by default '*'.
Returns
-------
Str2D
A new Str2D object with the Mandelbrot set.
See Also
--------
is_in_mandelbrot : Determines if the point is in the Mandelbrot set.
Examples
--------
Let's create a Mandelbrot set.
.. testcode::
str2d.mandelbrot(
44, 88,
(-2, .5),
(-1.25, 1.25),
'*'
).strip2d().box()
.. testoutput::
╭───────────────────────────────────────────────────────────────╮
│ **** │
│ * ***** * │
│ ********* │
│ ******** │
│ *** * * * ******* * * │
│ **** ******************** * * │
│ ******************************* │
│ ******************************* │
│ ************************************* │
│ * **************************************│
│ ***************************************│
│ *** ******* *************************************** │
│ ************* ****************************************│
│ **************** *****************************************│
│ ********************************************************** │
│************************************************************ │
│************************************************************ │
│ ********************************************************** │
│ **************** *****************************************│
│ ************* ****************************************│
│ *** ******* *************************************** │
│ ***************************************│
│ * **************************************│
│ ************************************* │
│ ******************************* │
│ ******************************* │
│ **** ******************** * * │
│ *** * * * ******* * * │
│ ******** │
│ ********* │
│ * ***** * │
│ **** │
╰───────────────────────────────────────────────────────────────╯
"""
mask = region(is_in_mandelbrot, height, width, x_range, y_range)
return Str2D(mask, char=char)
def pre(
s: Str2D, font_size: int = 1, bg: Optional[str] = None, fg: Optional[str] = None
) -> HTML:
"""Create a preformatted HTML object."""
this_id = str(uuid.uuid4())
bg = bg or "rgba(0, 0, 0, 0)"
fg = fg or "rgba(255, 255, 255, 1)"
return HTML(
dedent(
f"""\
<meta charset="UTF-8">
<style>
#_{this_id} {{
font-family: monospace;
font-size: {font_size}px;
background-color: {bg};
color: {fg};
width: fit-content;
height: fit-content;
}}
</style>
<pre id="_{this_id}">{s!s}</pre>
"""
)
)
[docs]
def traverse_path(path, box_style, depth=0):
"""Traverse a path and return a string representation of the path.
This function is a recursive function that traverses a path and returns a string
Parameters
----------
path : Path
The path to traverse.
box_style : BoxStyle
The style connecting lines.
depth : int, optional
The depth to traverse, by default 0.
Returns
-------
str
The string representation of the path.
"""
if depth > 0:
ls = list(path.iterdir())
k = len(ls)
to_be_joined = []
for i, p in enumerate(ls):
last = i == k - 1
char = box_style.value.ll if last else box_style.value.l
char += box_style.value.h
buff = (" " if last else box_style.value.v) + " "
pstr = Str2D(p.name)
if p.is_file():
to_be_joined.append(char + pstr)
elif p.is_dir():
head = char + pstr
tail = traverse_path(p, box_style, depth - 1)
if tail:
to_be_joined.append(head / (buff + tail))
else:
to_be_joined.append(head)
return Str2D.join_v(*to_be_joined)
return ""