Skip to content

Commit

Permalink
Add fmt::Display wrapper to pad/truncate using correct width
Browse files Browse the repository at this point in the history
Fixes #9
  • Loading branch information
Jules-Bertholet committed May 10, 2024
1 parent 3063422 commit 3425b61
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 51 deletions.
36 changes: 30 additions & 6 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,40 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build

- name: Build (all features)
run: cargo build --features display --verbose
- name: Run tests (all features)
run: cargo test --features display --verbose
- name: Check clippy (all features)
run: cargo clippy --features display --lib --tests --verbose

- name: Build (default features)
run: cargo build --verbose
- name: Run tests
- name: Run tests (default features)
run: cargo test --verbose
- name: Build docs
run: cargo doc
- name: Check clippy (default features)
run: cargo clippy --lib --tests --verbose

fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check formatting
run: cargo fmt --check
- name: Check clippy
run: cargo clippy --lib --tests

nightly:
env:
RUSTDOCFLAGS: -D warnings --cfg docsrs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install nightly
run: rustup toolchain add nightly && rustup +nightly component add clippy
- name: Build docs
run: cargo +nightly doc --features display --verbose
- name: Check benches
run: cargo +nightly clippy --benches --features display --verbose

regen:
runs-on: ubuntu-latest
Expand Down
28 changes: 0 additions & 28 deletions .travis.yml

This file was deleted.

16 changes: 11 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@ according to Unicode Standard Annex #11 rules.
"""
edition = "2021"

exclude = ["target/*", "Cargo.lock"]
exclude = ["/.github/*", "/target/*", "/Cargo.lock"]

[dependencies]
std = { version = "1.0", package = "rustc-std-workspace-std", optional = true }
core = { version = "1.0", package = "rustc-std-workspace-core", optional = true }
unicode-segmentation = { version = "1.11.0", optional = true }

compiler_builtins = { version = "0.1", optional = true }
core = { version = "1.0", package = "rustc-std-workspace-core", optional = true }
std = { version = "1.0", package = "rustc-std-workspace-std", optional = true }

[features]
default = []
rustc-dep-of-std = ['std', 'core', 'compiler_builtins']
display = ["dep:unicode-segmentation"]
rustc-dep-of-std = ["dep:compiler_builtins", "dep:core", "dep:std"]

# Legacy, now a no-op
no_std = []

[package.metadata.docs.rs]
features = ["display"]
rustdoc-args = ["--cfg", "docsrs"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `unicode-width`

[![Build status](https://github.com/unicode-rs/unicode-width/actions/workflows/rust.yml/badge.svg)](https://travis-ci.org/unicode-rs/unicode-width)
[![Build status](https://github.com/unicode-rs/unicode-width/actions/workflows/rust.yml/badge.svg)](https://github.com/unicode-rs/unicode-width/actions/workflows/rust.yml)
[![crates.io version](https://img.shields.io/crates/v/unicode-width)](https://crates.io/crates/unicode-width)
[![Docs status](https://img.shields.io/docsrs/unicode-width)](https://docs.rs/unicode-width/)

Expand Down
17 changes: 7 additions & 10 deletions benches/benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@
#![feature(test)]

extern crate test;

use std::iter;

use test::Bencher;

use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

#[bench]
fn cargo(b: &mut Bencher) {
let string = iter::repeat('a').take(4096).collect::<String>();
let string = "a".repeat(4096);

b.iter(|| {
for c in string.chars() {
Expand All @@ -31,7 +28,7 @@ fn cargo(b: &mut Bencher) {
#[bench]
#[allow(deprecated)]
fn stdlib(b: &mut Bencher) {
let string = iter::repeat('a').take(4096).collect::<String>();
let string = "a".repeat(4096);

b.iter(|| {
for c in string.chars() {
Expand All @@ -42,7 +39,7 @@ fn stdlib(b: &mut Bencher) {

#[bench]
fn simple_if(b: &mut Bencher) {
let string = iter::repeat('a').take(4096).collect::<String>();
let string = "a".repeat(4096);

b.iter(|| {
for c in string.chars() {
Expand All @@ -53,7 +50,7 @@ fn simple_if(b: &mut Bencher) {

#[bench]
fn simple_match(b: &mut Bencher) {
let string = iter::repeat('a').take(4096).collect::<String>();
let string = "a".repeat(4096);

b.iter(|| {
for c in string.chars() {
Expand Down Expand Up @@ -81,9 +78,9 @@ fn simple_width_if(c: char) -> Option<usize> {
#[inline]
fn simple_width_match(c: char) -> Option<usize> {
match c as u32 {
cu if cu == 0 => Some(0),
cu if cu < 0x20 => None,
cu if cu < 0x7f => Some(1),
0 => Some(0),
1..=0x1F => None,
0x20..=0x7E => Some(1),
_ => UnicodeWidthChar::width(c),
}
}
Expand Down
157 changes: 157 additions & 0 deletions src/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use core::fmt::{self, Write};

use unicode_segmentation::UnicodeSegmentation;

use crate::{UnicodeWidthChar, UnicodeWidthStr};

/// A wrapper around a [`str`] with a [`fmt::Display`] impl
/// that performs padding, truncation, and alignment based on
/// the string width according to this crate (non-CJK).
///
/// Produced via [`UnicodeWidthStr::using_width`];
/// see its documentation for more.
#[derive(PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct StrWithWidth(str);

impl StrWithWidth {
/// The advance width of the `string`
/// (equivalent to [`UnicodeWidthStr::width`]).
#[inline]
pub fn width(&self) -> usize {
self.0.width()
}
}

impl PartialEq<str> for StrWithWidth {
#[inline]
fn eq(&self, other: &str) -> bool {
&self.0 == other
}
}

impl AsRef<str> for StrWithWidth {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}

impl AsMut<str> for StrWithWidth {
#[inline]
fn as_mut(&mut self) -> &mut str {
&mut self.0
}
}

impl AsRef<StrWithWidth> for str {
#[inline]
fn as_ref(&self) -> &StrWithWidth {
// SAFETY: `repr(transparent)` ensures compatible types
unsafe { core::mem::transmute(self) }
}
}

impl AsMut<StrWithWidth> for str {
#[inline]
fn as_mut(&mut self) -> &mut StrWithWidth {
// SAFETY: `repr(transparent)` ensures compatible types
unsafe { core::mem::transmute(self) }
}
}

impl fmt::Display for StrWithWidth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Fast path
if f.width().is_none() && f.precision().is_none() {
return f.write_str(&self.0);
}

// Truncate the string to maximum width
let (truncated, truncated_width) = if let Some(max_width) = f.precision() {
let mut truncated_width: usize = 0;
let mut truncated = &self.0;
for (seg_offset, seg) in self.0.grapheme_indices(true) {
let new_width = truncated_width + seg.width();
if new_width > max_width {
truncated = &self.0[..seg_offset];
break;
} else {
truncated_width = new_width;
}
}
(truncated, truncated_width)
} else {
(&self.0, self.0.width())
};

// Pad the string to minimum width
if let Some(padding) = f
.width()
.and_then(|min_width| min_width.checked_sub(truncated_width))
.filter(|&padding| padding > 0)
{
let align = f.align().unwrap_or(fmt::Alignment::Left);

let mut fill_char = f.fill();
let mut fill_char_width = fill_char.width().unwrap_or(1);

// If we try to fill with a zero-sized char, we'll never succeed, so fall back to space
if fill_char_width == 0 {
fill_char = ' ';
fill_char_width = 1;
}

let (pre_pre_pad, pre_pad, post_pad, post_post_pad) = match align {
fmt::Alignment::Left => {
(0, 0, padding % fill_char_width, padding / fill_char_width)
}
fmt::Alignment::Right => {
(padding / fill_char_width, padding % fill_char_width, 0, 0)
}
fmt::Alignment::Center => {
let (left_padding, right_padding) = (padding / 2, (padding + 1) / 2);
let (pre_pre_pad, mut pre_pad, mut post_pad, mut post_post_pad) = {
(
left_padding / fill_char_width,
left_padding % fill_char_width,
right_padding % fill_char_width,
right_padding / fill_char_width,
)
};
if let Some(diff) = pre_pad.checked_sub(fill_char_width - post_pad) {
pre_pad = 0;
post_pad = diff;
post_post_pad += 1;
}
(pre_pre_pad, pre_pad, post_pad, post_post_pad)
}
};

for _ in 0..pre_pre_pad {
f.write_char(fill_char)?;
}
for _ in 0..pre_pad {
f.write_char(' ')?;
}
f.write_str(truncated)?;
for _ in 0..post_pad {
f.write_char(' ')?;
}
for _ in 0..post_post_pad {
f.write_char(fill_char)?;
}

Ok(())
} else {
f.write_str(truncated)
}
}
}

impl fmt::Debug for StrWithWidth {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
Loading

0 comments on commit 3425b61

Please sign in to comment.