Skip to content

Commit 484656d

Browse files
committed
Rename Processing helper to Aggregation
1 parent a6b60e6 commit 484656d

6 files changed

Lines changed: 163 additions & 168 deletions

File tree

‎lib/youplot/aggregation.rb‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# frozen_string_literal: true
2+
3+
module YouPlot
4+
module Aggregation
5+
module_function
6+
7+
def count_values(arr, tally: true, reverse: false)
8+
# tally was added in Ruby 2.7
9+
result = \
10+
if tally && Enumerable.method_defined?(:tally)
11+
arr.tally
12+
else
13+
# value_counts Enumerable::Statistics
14+
arr.value_counts(dropna: false)
15+
end
16+
17+
sort_cache = {}
18+
19+
# sorting
20+
result = result.sort do |a, b|
21+
# compare values
22+
r = b[1] <=> a[1]
23+
# If the values are the same, compare by name
24+
r = natural_compare(a[0], b[0], sort_cache) if r.zero?
25+
r
26+
end
27+
28+
# --reverse option
29+
result.reverse! if reverse
30+
31+
# prepare for barplot
32+
result.transpose
33+
end
34+
35+
# Natural order comparison for tie-breaking when counts are equal.
36+
# Fast paths handle text-only and pure numeric labels.
37+
# Mixed labels still use chunked comparison (e.g. "chr1" vs "chr10").
38+
def natural_compare(a, b, cache = nil)
39+
aa = natural_sort_key(a, cache)
40+
bb = natural_sort_key(b, cache)
41+
42+
# Fast path: both labels are text-only, so plain string comparison is enough.
43+
return aa[:string] <=> bb[:string] if aa[:type] == :text && bb[:type] == :text
44+
45+
# Fast path: both labels are pure numbers, so compare numerically first.
46+
if aa[:type] == :numeric && bb[:type] == :numeric
47+
r = aa[:numeric] <=> bb[:numeric]
48+
return r unless r.zero?
49+
50+
# Tiebreaker for equivalent numeric values (e.g. "1" and "01")
51+
return aa[:string] <=> bb[:string]
52+
end
53+
54+
# Fallback path: at least one label mixes text and digits.
55+
ta = ensure_natural_tokens(aa)
56+
tb = ensure_natural_tokens(bb)
57+
max = [ta.size, tb.size].max
58+
59+
0.upto(max - 1) do |i|
60+
xa = ta[i]
61+
xb = tb[i]
62+
63+
return -1 if xa.nil?
64+
return 1 if xb.nil?
65+
66+
r = if xa[0] == :num && xb[0] == :num
67+
compare_integer_strings(xa[1], xb[1])
68+
else
69+
xa[1] <=> xb[1]
70+
end
71+
72+
return r unless r.zero?
73+
end
74+
75+
aa[:string] <=> bb[:string]
76+
end
77+
78+
# Classifies a value for natural sorting and caches the result per label.
79+
def natural_sort_key(value, cache = nil)
80+
str = value.to_s
81+
return cache[str] if cache && cache.key?(str)
82+
83+
key = if str.match?(/\d/)
84+
numeric = parse_numeric(str)
85+
if numeric
86+
# Pure numeric labels get a dedicated fast path.
87+
{ type: :numeric, string: str, numeric: numeric }
88+
else
89+
# Mixed labels fall back to chunked natural comparison.
90+
{ type: :mixed, string: str, tokens: nil }
91+
end
92+
else
93+
# Text-only labels get a dedicated fast path.
94+
{ type: :text, string: str, tokens: nil }
95+
end
96+
97+
cache ? cache[str] = key : key
98+
end
99+
100+
# Memoizes token pairs for fallback chunked comparison.
101+
def ensure_natural_tokens(key)
102+
key[:tokens] ||= natural_tokens(key[:string])
103+
end
104+
105+
# Parses a string as a numeric value if it matches pure number format.
106+
# Returns Float or nil.
107+
def parse_numeric(str)
108+
return nil unless str.match?(/\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)\z/)
109+
110+
str.to_f
111+
end
112+
113+
# Splits a string into [type, token] pairs for natural comparison.
114+
# Type is :num for digit-only chunks, :text for anything else.
115+
# E.g. "chr10" => [[:text, "chr"], [:num, "10"]]
116+
def natural_tokens(str)
117+
str.scan(/\d+|\D+/).map do |tok|
118+
kind = tok.match?(/\A\d+\z/) ? :num : :text
119+
[kind, tok]
120+
end
121+
end
122+
123+
# Compares two numeric strings, handling leading zeros.
124+
# Order: by length (sans leading zeros), then numeric value, then original.
125+
def compare_integer_strings(a, b)
126+
aa = a.sub(/\A0+/, '')
127+
bb = b.sub(/\A0+/, '')
128+
aa = '0' if aa.empty?
129+
bb = '0' if bb.empty?
130+
131+
r = aa.length <=> bb.length
132+
return r unless r.zero?
133+
134+
r = aa <=> bb
135+
return r unless r.zero?
136+
137+
a <=> b
138+
end
139+
end
140+
end

‎lib/youplot/backends/processing.rb‎

Lines changed: 0 additions & 143 deletions
This file was deleted.

‎lib/youplot/backends/unicode_plot.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# UnicodePlot - Plot your data by Unicode characters
44
# https://github.com/red-data-tools/unicode_plot.rb
55

6-
require_relative 'processing'
6+
require_relative '../aggregation'
77
require 'unicode_plot'
88

99
# If the line color is specified as a number, the program will display an error
@@ -40,7 +40,7 @@ def barplot(data, params, fmt = nil, count: false, reverse: false)
4040
series = data.series
4141
# `uplot count`
4242
if count
43-
series = Processing.count_values(series[0], reverse: reverse)
43+
series = YouPlot::Aggregation.count_values(series[0], reverse: reverse)
4444
params.title = headers[0] if headers
4545
end
4646
if series.size == 1

‎sig/youplot/aggregation.rbs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module YouPlot
2+
module Aggregation
3+
def self.count_values: (Enumerable[untyped], ?tally: bool, ?reverse: bool) -> Array[Array[untyped]]
4+
end
5+
end

‎sig/youplot/backends/processing.rbs‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)