Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rust): right-align numeric columns #7475

Merged
merged 11 commits into from
Oct 16, 2023
1 change: 1 addition & 0 deletions crates/polars-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub(crate) const FMT_MAX_COLS: &str = "POLARS_FMT_MAX_COLS";
pub(crate) const FMT_MAX_ROWS: &str = "POLARS_FMT_MAX_ROWS";
pub(crate) const FMT_STR_LEN: &str = "POLARS_FMT_STR_LEN";
pub(crate) const FMT_TABLE_CELL_ALIGNMENT: &str = "POLARS_FMT_TABLE_CELL_ALIGNMENT";
pub(crate) const FMT_TABLE_CELL_NUMERIC_ALIGNMENT: &str = "POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT";
pub(crate) const FMT_TABLE_DATAFRAME_SHAPE_BELOW: &str = "POLARS_FMT_TABLE_DATAFRAME_SHAPE_BELOW";
pub(crate) const FMT_TABLE_FORMATTING: &str = "POLARS_FMT_TABLE_FORMATTING";
pub(crate) const FMT_TABLE_HIDE_COLUMN_DATA_TYPES: &str = "POLARS_FMT_TABLE_HIDE_COLUMN_DATA_TYPES";
Expand Down
47 changes: 36 additions & 11 deletions crates/polars-core/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::RwLock;
use std::{fmt, str};

#[cfg(any(
Expand Down Expand Up @@ -33,6 +34,7 @@ pub enum FloatFmt {
Full,
}
static FLOAT_FMT: AtomicU8 = AtomicU8::new(FloatFmt::Mixed as u8);
static FLOAT_PRECISION: RwLock<Option<usize>> = RwLock::new(None);

pub fn get_float_fmt() -> FloatFmt {
match FLOAT_FMT.load(Ordering::Relaxed) {
Expand All @@ -42,10 +44,18 @@ pub fn get_float_fmt() -> FloatFmt {
}
}

pub fn get_float_precision() -> Option<usize> {
*FLOAT_PRECISION.read().unwrap()
}

pub fn set_float_fmt(fmt: FloatFmt) {
FLOAT_FMT.store(fmt as u8, Ordering::Relaxed)
}

pub fn set_float_precision(precision: Option<usize>) {
*FLOAT_PRECISION.write().unwrap() = precision;
}

macro_rules! format_array {
($f:ident, $a:expr, $dtype:expr, $name:expr, $array_type:expr) => {{
write!(
Expand Down Expand Up @@ -655,19 +665,24 @@ impl Display for DataFrame {
}

// set alignment of cells, if defined
if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok() {
// for (column_index, column) in table.column_iter_mut().enumerate() {
if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok()
| std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT).is_ok()
{
let str_preset = std::env::var(FMT_TABLE_CELL_ALIGNMENT)
.unwrap_or_else(|_| "DEFAULT".to_string());
for column in table.column_iter_mut() {
if str_preset == "RIGHT" {
column.set_cell_alignment(CellAlignment::Right);
} else if str_preset == "LEFT" {
column.set_cell_alignment(CellAlignment::Left);
} else if str_preset == "CENTER" {
column.set_cell_alignment(CellAlignment::Center);
} else {
column.set_cell_alignment(CellAlignment::Left);
let num_preset = std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT)
.unwrap_or_else(|_| str_preset.to_string());
for (column_index, column) in table.column_iter_mut().enumerate() {
let dtype = fields[column_index].data_type();
let mut preset = str_preset.as_str();
if dtype.is_numeric() {
preset = num_preset.as_str();
}
match preset {
"RIGHT" => column.set_cell_alignment(CellAlignment::Right),
"LEFT" => column.set_cell_alignment(CellAlignment::Left),
"CENTER" => column.set_cell_alignment(CellAlignment::Center),
_ => {},
}
}
}
Expand Down Expand Up @@ -709,6 +724,16 @@ const SCIENTIFIC_BOUND: f64 = 999999.0;

fn fmt_float<T: Num + NumCast>(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result {
let v: f64 = NumCast::from(v).unwrap();

let float_precision = get_float_precision();

if let Some(precision) = float_precision {
if format!("{v:.precision$}", precision = precision).len() > 19 {
return write!(f, "{v:>width$.precision$e}", precision = precision);
}
return write!(f, "{v:>width$.precision$}", precision = precision);
}

if matches!(get_float_fmt(), FloatFmt::Full) {
return write!(f, "{v:>width$}");
}
Expand Down
111 changes: 108 additions & 3 deletions py-polars/polars/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@
from polars.utils.various import normalize_filepath


# dummy func required (so docs build)
# dummy funcs required here (so that docs build)
def _get_float_fmt() -> str: # pragma: no cover
return "n/a"


def _get_float_precision() -> int:
return -1


# note: module not available when building docs
with contextlib.suppress(ImportError):
from polars.polars import get_float_fmt as _get_float_fmt # type: ignore[no-redef]
from polars.polars import ( # type: ignore[no-redef]
get_float_precision as _get_float_precision,
)
from polars.polars import set_float_fmt as _set_float_fmt
from polars.polars import set_float_precision as _set_float_precision


if sys.version_info >= (3, 10):
Expand Down Expand Up @@ -60,7 +68,9 @@ def _get_float_fmt() -> str: # pragma: no cover
"POLARS_FMT_MAX_COLS",
"POLARS_FMT_MAX_ROWS",
"POLARS_FMT_STR_LEN",
"POLARS_FMT_NUM_LEN",
"POLARS_FMT_TABLE_CELL_ALIGNMENT",
"POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT",
"POLARS_FMT_TABLE_DATAFRAME_SHAPE_BELOW",
"POLARS_FMT_TABLE_FORMATTING",
"POLARS_FMT_TABLE_HIDE_COLUMN_DATA_TYPES",
Expand All @@ -76,7 +86,10 @@ def _get_float_fmt() -> str: # pragma: no cover

# vars that set the rust env directly should declare themselves here as the Config
# method name paired with a callable that returns the current state of that value:
_POLARS_CFG_DIRECT_VARS = {"set_fmt_float": _get_float_fmt}
_POLARS_CFG_DIRECT_VARS = {
"set_fmt_float": _get_float_fmt,
"set_float_precision": _get_float_precision,
}


class Config(contextlib.ContextDecorator):
Expand Down Expand Up @@ -252,6 +265,7 @@ def restore_defaults(cls) -> type[Config]:

# apply any 'direct' setting values
cls.set_fmt_float()
cls.set_float_precision()
return cls

@classmethod
Expand Down Expand Up @@ -347,7 +361,7 @@ def state(
}
if not env_only:
for cfg_methodname, get_value in _POLARS_CFG_DIRECT_VARS.items():
config_state[cfg_methodname] = get_value()
config_state[cfg_methodname] = get_value() # type: ignore[assignment]

return config_state

Expand Down Expand Up @@ -427,6 +441,47 @@ def set_auto_structify(cls, active: bool | None = False) -> type[Config]:
os.environ["POLARS_AUTO_STRUCTIFY"] = str(int(active))
return cls

@classmethod
def set_float_precision(cls, precision: int | None = None) -> type[Config]:
"""
Control the number of decimal places displayed for floating point values.

Parameters
----------
precision : int
Number of decimal places to display; set to ``None`` to revert to the
default/standard behaviour.

Notes
-----
When setting this to a larger value you should ensure that you are aware of both
the limitations of floating point representations, and of the precision of the
data that you are looking at.

This setting only applies to Float32 and Float64 dtypes; it does not cover
Decimal dtype values (which are displayed at their native level of precision).

Examples
--------
>>> from math import pi, e
>>> df = pl.DataFrame({"const": ["pi", "e"], "value": [pi, e]})
>>> with pl.Config(float_precision=15):
... print(repr(df))
...
shape: (2, 2)
┌───────┬───────────────────┐
│ const ┆ value │
│ --- ┆ --- │
│ str ┆ f64 │
╞═══════╪═══════════════════╡
│ pi ┆ 3.141592653589793 │
│ e ┆ 2.718281828459045 │
└───────┴───────────────────┘

"""
_set_float_precision(precision)
return cls

@classmethod
def set_fmt_float(cls, fmt: FloatFmt | None = "mixed") -> type[Config]:
"""
Expand Down Expand Up @@ -598,6 +653,56 @@ def set_tbl_cell_alignment(
os.environ["POLARS_FMT_TABLE_CELL_ALIGNMENT"] = format
return cls

@classmethod
def set_tbl_cell_numeric_alignment(
cls, format: Literal["LEFT", "CENTER", "RIGHT"] | None
) -> type[Config]:
"""
Set table cell alignment for numeric columns.

Parameters
----------
format : str
* "LEFT": left aligned
* "CENTER": center aligned
* "RIGHT": right aligned

Examples
--------
>>> from datetime import date
>>> df = pl.DataFrame(
... {
... "abc": [11, 2, 333],
... "mno": [date.today(), None, date.today()],
... "xyz": [True, False, None],
... }
... )
>>> pl.Config.set_tbl_cell_numeric_alignment("RIGHT") # doctest: +SKIP
# ...
# shape: (3, 3)
# ┌─────┬────────────┬───────┐
# │ abc ┆ mno ┆ xyz │
# │ --- ┆ --- ┆ --- │
# │ i64 ┆ date ┆ bool │
# ╞═════╪════════════╪═══════╡
# │ 11 ┆ 2023-09-05 ┆ true │
# │ 2 ┆ null ┆ false │
# │ 333 ┆ 2023-09-05 ┆ null │
# └─────┴────────────┴───────┘

Raises
------
KeyError: if alignment string not recognised.

"""
if format is None:
os.environ.pop("POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT", None)
elif format not in {"LEFT", "CENTER", "RIGHT"}:
raise ValueError(f"invalid alignment: {format!r}")
else:
os.environ["POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT"] = format
return cls

@classmethod
def set_tbl_cols(cls, n: int | None) -> type[Config]:
"""
Expand Down
13 changes: 13 additions & 0 deletions py-polars/src/functions/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ pub fn get_float_fmt() -> PyResult<String> {
};
Ok(strfmt.to_string())
}

#[pyfunction]
pub fn set_float_precision(precision: Option<usize>) -> PyResult<()> {
use polars_core::fmt::set_float_precision;
set_float_precision(precision);
Ok(())
}

#[pyfunction]
pub fn get_float_precision() -> PyResult<Option<usize>> {
use polars_core::fmt::get_float_precision;
Ok(get_float_precision())
}
4 changes: 4 additions & 0 deletions py-polars/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ fn polars(py: Python, m: &PyModule) -> PyResult<()> {
.unwrap();
m.add_wrapped(wrap_pyfunction!(functions::meta::get_float_fmt))
.unwrap();
m.add_wrapped(wrap_pyfunction!(functions::meta::set_float_precision))
.unwrap();
m.add_wrapped(wrap_pyfunction!(functions::meta::get_float_precision))
.unwrap();

// Functions - misc
m.add_wrapped(wrap_pyfunction!(functions::misc::dtype_str_repr))
Expand Down
Loading