Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to this project are documented in this file.

## [1.2.0] - 2025-12-20
### Changed
- `reverse/1` now uses the BEAM stdlib grapheme segmentation (`string.to_graphemes`) for better Unicode correctness and consistency across the library.

### Performance
- Optimized `count/3` internals to avoid repeated `list.length` calls inside recursive loops, improving performance on long strings.

### CI
- Pinned Gleam version in CI and fixed build cache path/key for more reproducible and faster runs.

Contributed by: Daniele (`lupodevelop`)

## [1.1.1] - 2025-11-30
### Fixed
- Robustness fixes for grapheme-aware utilities; resolved parity issues in `ends_with/2` for complex ZWJ sequences.
Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "str"
version = "1.1.1"
version = "1.2.0"

# Project metadata (fill or replace placeholders before publishing)
description = "Unicode-aware string utilities for Gleam: grapheme-safe operations, pragmatic ASCII transliteration, and slug generation."
Expand Down
42 changes: 34 additions & 8 deletions src/str/core.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import gleam/int
import gleam/list
import gleam/string
import str/tokenize

/// Detects if a grapheme cluster likely contains emoji components.
///
Expand Down Expand Up @@ -195,24 +194,51 @@ fn count_loop(
overlapping: Bool,
acc: Int,
) -> Int {
case list.length(hs) < nd_len {
count_loop_with_len(hs, list.length(hs), nd, nd_len, overlapping, acc)
}

fn count_loop_with_len(
hs: List(String),
hs_len: Int,
nd: List(String),
nd_len: Int,
overlapping: Bool,
acc: Int,
) -> Int {
case hs_len < nd_len {
True -> acc
False ->
case list.take(hs, nd_len) == nd {
True ->
case overlapping {
True ->
count_loop(list.drop(hs, 1), nd, nd_len, overlapping, acc + 1)
count_loop_with_len(
list.drop(hs, 1),
hs_len - 1,
nd,
nd_len,
overlapping,
acc + 1,
)
False ->
count_loop(
count_loop_with_len(
list.drop(hs, nd_len),
hs_len - nd_len,
nd,
nd_len,
overlapping,
acc + 1,
)
}
False -> count_loop(list.drop(hs, 1), nd, nd_len, overlapping, acc)
False ->
count_loop_with_len(
list.drop(hs, 1),
hs_len - 1,
nd,
nd_len,
overlapping,
acc,
)
}
}
}
Expand Down Expand Up @@ -413,10 +439,10 @@ pub fn truncate_default(text: String, max_len: Int) -> String {
/// reverse("👨‍👩‍👧‍👦") -> "👨‍👩‍👧‍👦"
///
pub fn reverse(text: String) -> String {
let clusters = tokenize.chars(text)
clusters
text
|> string.to_graphemes
|> list.reverse
|> list.fold("", fn(acc, s) { acc <> s })
|> string.concat
}

// ============================================================================
Expand Down