From 3e2ab1258b932863c01042d116bf40f63522eabd Mon Sep 17 00:00:00 2001 From: ericgpks Date: Mon, 28 Apr 2025 22:01:16 +0900 Subject: [PATCH 1/5] feat: add necessary gems to introduce rbs to this repository To add types, I would like to have help from typeprof to define rbs files for current library code that's why I added both steep and typeprof to Gemfile. --- Gemfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 43abf00..54911d2 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,11 @@ source 'https://rubygems.org' # Specify your gem's dependencies in youplot.gemspec gemspec +group :development do + gem 'steep', require: false + gem 'typeprof' +end + group :test do gem 'rake' gem 'simplecov' From 674daa84eb9f89414b0009a3cd9835ac56c824b1 Mon Sep 17 00:00:00 2001 From: ericgpks Date: Mon, 28 Apr 2025 22:11:31 +0900 Subject: [PATCH 2/5] feat: add Steepfile with the minimum configuration This file indicates that type definition files are placed in the `sig` directory and the actual implementation files that I want to define types for are located in the `lib` directory. Additionally, it is configured to disable type error checking so as not to raise much errors. --- Steepfile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Steepfile diff --git a/Steepfile b/Steepfile new file mode 100644 index 0000000..edee931 --- /dev/null +++ b/Steepfile @@ -0,0 +1,8 @@ +D = Steep::Diagnostic + +target :lib do + signature "sig" + check "lib" # Directory name to check + + configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting +end From 448c1e90795e6b8a9733d781cd77860c615ad2fc Mon Sep 17 00:00:00 2001 From: ericgpks Date: Mon, 28 Apr 2025 22:31:43 +0900 Subject: [PATCH 3/5] feat: add processing.rbs as a type definition file for processing.rb As a first step toward adopting rbs for this project, added type a definition for a simple method. More type definitions will be added incrementally. --- sig/youplot/backends/processing.rbs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 sig/youplot/backends/processing.rbs diff --git a/sig/youplot/backends/processing.rbs b/sig/youplot/backends/processing.rbs new file mode 100644 index 0000000..9830361 --- /dev/null +++ b/sig/youplot/backends/processing.rbs @@ -0,0 +1,7 @@ +module YouPlot + module Backends + module Processing + def count_values: ([], ?tally: true, ?reverse: false) -> Array[Array[untyped]] + end + end +end From 764fb977f3d117ca90d34359d84cab21d56a60f1 Mon Sep 17 00:00:00 2001 From: ericgpks Date: Wed, 30 Apr 2025 23:20:59 +0900 Subject: [PATCH 4/5] fix: add condition for using steep and typeprof to pass CI with all Ruby versions --- Gemfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 54911d2..90a7c73 100644 --- a/Gemfile +++ b/Gemfile @@ -6,8 +6,10 @@ source 'https://rubygems.org' gemspec group :development do - gem 'steep', require: false - gem 'typeprof' + if RUBY_VERSION >= '3.0' + gem 'steep', require: false + gem 'typeprof' + end end group :test do From 484656db3d5ab3fb336815dfbcb341e2ee4789b0 Mon Sep 17 00:00:00 2001 From: kojix2 <2xijok@gmail.com> Date: Sat, 23 May 2026 18:32:02 +0900 Subject: [PATCH 5/5] Rename Processing helper to Aggregation --- lib/youplot/aggregation.rb | 140 +++++++++++++++++ lib/youplot/backends/processing.rb | 143 ------------------ lib/youplot/backends/unicode_plot.rb | 4 +- sig/youplot/aggregation.rbs | 5 + sig/youplot/backends/processing.rbs | 7 - ...processing_test.rb => aggregation_test.rb} | 32 ++-- 6 files changed, 163 insertions(+), 168 deletions(-) create mode 100644 lib/youplot/aggregation.rb delete mode 100644 lib/youplot/backends/processing.rb create mode 100644 sig/youplot/aggregation.rbs delete mode 100644 sig/youplot/backends/processing.rbs rename test/youplot/{backends/processing_test.rb => aggregation_test.rb} (79%) diff --git a/lib/youplot/aggregation.rb b/lib/youplot/aggregation.rb new file mode 100644 index 0000000..1fe033b --- /dev/null +++ b/lib/youplot/aggregation.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module YouPlot + module Aggregation + module_function + + def count_values(arr, tally: true, reverse: false) + # tally was added in Ruby 2.7 + result = \ + if tally && Enumerable.method_defined?(:tally) + arr.tally + else + # value_counts Enumerable::Statistics + arr.value_counts(dropna: false) + end + + sort_cache = {} + + # sorting + result = result.sort do |a, b| + # compare values + r = b[1] <=> a[1] + # If the values are the same, compare by name + r = natural_compare(a[0], b[0], sort_cache) if r.zero? + r + end + + # --reverse option + result.reverse! if reverse + + # prepare for barplot + result.transpose + end + + # Natural order comparison for tie-breaking when counts are equal. + # Fast paths handle text-only and pure numeric labels. + # Mixed labels still use chunked comparison (e.g. "chr1" vs "chr10"). + def natural_compare(a, b, cache = nil) + aa = natural_sort_key(a, cache) + bb = natural_sort_key(b, cache) + + # Fast path: both labels are text-only, so plain string comparison is enough. + return aa[:string] <=> bb[:string] if aa[:type] == :text && bb[:type] == :text + + # Fast path: both labels are pure numbers, so compare numerically first. + if aa[:type] == :numeric && bb[:type] == :numeric + r = aa[:numeric] <=> bb[:numeric] + return r unless r.zero? + + # Tiebreaker for equivalent numeric values (e.g. "1" and "01") + return aa[:string] <=> bb[:string] + end + + # Fallback path: at least one label mixes text and digits. + ta = ensure_natural_tokens(aa) + tb = ensure_natural_tokens(bb) + max = [ta.size, tb.size].max + + 0.upto(max - 1) do |i| + xa = ta[i] + xb = tb[i] + + return -1 if xa.nil? + return 1 if xb.nil? + + r = if xa[0] == :num && xb[0] == :num + compare_integer_strings(xa[1], xb[1]) + else + xa[1] <=> xb[1] + end + + return r unless r.zero? + end + + aa[:string] <=> bb[:string] + end + + # Classifies a value for natural sorting and caches the result per label. + def natural_sort_key(value, cache = nil) + str = value.to_s + return cache[str] if cache && cache.key?(str) + + key = if str.match?(/\d/) + numeric = parse_numeric(str) + if numeric + # Pure numeric labels get a dedicated fast path. + { type: :numeric, string: str, numeric: numeric } + else + # Mixed labels fall back to chunked natural comparison. + { type: :mixed, string: str, tokens: nil } + end + else + # Text-only labels get a dedicated fast path. + { type: :text, string: str, tokens: nil } + end + + cache ? cache[str] = key : key + end + + # Memoizes token pairs for fallback chunked comparison. + def ensure_natural_tokens(key) + key[:tokens] ||= natural_tokens(key[:string]) + end + + # Parses a string as a numeric value if it matches pure number format. + # Returns Float or nil. + def parse_numeric(str) + return nil unless str.match?(/\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)\z/) + + str.to_f + end + + # Splits a string into [type, token] pairs for natural comparison. + # Type is :num for digit-only chunks, :text for anything else. + # E.g. "chr10" => [[:text, "chr"], [:num, "10"]] + def natural_tokens(str) + str.scan(/\d+|\D+/).map do |tok| + kind = tok.match?(/\A\d+\z/) ? :num : :text + [kind, tok] + end + end + + # Compares two numeric strings, handling leading zeros. + # Order: by length (sans leading zeros), then numeric value, then original. + def compare_integer_strings(a, b) + aa = a.sub(/\A0+/, '') + bb = b.sub(/\A0+/, '') + aa = '0' if aa.empty? + bb = '0' if bb.empty? + + r = aa.length <=> bb.length + return r unless r.zero? + + r = aa <=> bb + return r unless r.zero? + + a <=> b + end + end +end diff --git a/lib/youplot/backends/processing.rb b/lib/youplot/backends/processing.rb deleted file mode 100644 index 9585c5d..0000000 --- a/lib/youplot/backends/processing.rb +++ /dev/null @@ -1,143 +0,0 @@ -# frozen_string_literal: true - -module YouPlot - # plotting functions. - module Backends - module Processing - module_function - - def count_values(arr, tally: true, reverse: false) - # tally was added in Ruby 2.7 - result = \ - if tally && Enumerable.method_defined?(:tally) - arr.tally - else - # value_counts Enumerable::Statistics - arr.value_counts(dropna: false) - end - - sort_cache = {} - - # sorting - result = result.sort do |a, b| - # compare values - r = b[1] <=> a[1] - # If the values are the same, compare by name - r = natural_compare(a[0], b[0], sort_cache) if r.zero? - r - end - - # --reverse option - result.reverse! if reverse - - # prepare for barplot - result.transpose - end - - # Natural order comparison for tie-breaking when counts are equal. - # Fast paths handle text-only and pure numeric labels. - # Mixed labels still use chunked comparison (e.g. "chr1" vs "chr10"). - def natural_compare(a, b, cache = nil) - aa = natural_sort_key(a, cache) - bb = natural_sort_key(b, cache) - - # Fast path: both labels are text-only, so plain string comparison is enough. - return aa[:string] <=> bb[:string] if aa[:type] == :text && bb[:type] == :text - - # Fast path: both labels are pure numbers, so compare numerically first. - if aa[:type] == :numeric && bb[:type] == :numeric - r = aa[:numeric] <=> bb[:numeric] - return r unless r.zero? - - # Tiebreaker for equivalent numeric values (e.g. "1" and "01") - return aa[:string] <=> bb[:string] - end - - # Fallback path: at least one label mixes text and digits. - ta = ensure_natural_tokens(aa) - tb = ensure_natural_tokens(bb) - max = [ta.size, tb.size].max - - 0.upto(max - 1) do |i| - xa = ta[i] - xb = tb[i] - - return -1 if xa.nil? - return 1 if xb.nil? - - r = if xa[0] == :num && xb[0] == :num - compare_integer_strings(xa[1], xb[1]) - else - xa[1] <=> xb[1] - end - - return r unless r.zero? - end - - aa[:string] <=> bb[:string] - end - - # Classifies a value for natural sorting and caches the result per label. - def natural_sort_key(value, cache = nil) - str = value.to_s - return cache[str] if cache && cache.key?(str) - - key = if str.match?(/\d/) - numeric = parse_numeric(str) - if numeric - # Pure numeric labels get a dedicated fast path. - { type: :numeric, string: str, numeric: numeric } - else - # Mixed labels fall back to chunked natural comparison. - { type: :mixed, string: str, tokens: nil } - end - else - # Text-only labels get a dedicated fast path. - { type: :text, string: str, tokens: nil } - end - - cache ? cache[str] = key : key - end - - # Memoizes token pairs for fallback chunked comparison. - def ensure_natural_tokens(key) - key[:tokens] ||= natural_tokens(key[:string]) - end - - # Parses a string as a numeric value if it matches pure number format. - # Returns Float or nil. - def parse_numeric(str) - return nil unless str.match?(/\A[+-]?(?:\d+(?:\.\d+)?|\.\d+)\z/) - - str.to_f - end - - # Splits a string into [type, token] pairs for natural comparison. - # Type is :num for digit-only chunks, :text for anything else. - # E.g. "chr10" => [[:text, "chr"], [:num, "10"]] - def natural_tokens(str) - str.scan(/\d+|\D+/).map do |tok| - kind = tok.match?(/\A\d+\z/) ? :num : :text - [kind, tok] - end - end - - # Compares two numeric strings, handling leading zeros. - # Order: by length (sans leading zeros), then numeric value, then original. - def compare_integer_strings(a, b) - aa = a.sub(/\A0+/, '') - bb = b.sub(/\A0+/, '') - aa = '0' if aa.empty? - bb = '0' if bb.empty? - - r = aa.length <=> bb.length - return r unless r.zero? - - r = aa <=> bb - return r unless r.zero? - - a <=> b - end - end - end -end diff --git a/lib/youplot/backends/unicode_plot.rb b/lib/youplot/backends/unicode_plot.rb index 7ba22b8..a41ed9f 100644 --- a/lib/youplot/backends/unicode_plot.rb +++ b/lib/youplot/backends/unicode_plot.rb @@ -3,7 +3,7 @@ # UnicodePlot - Plot your data by Unicode characters # https://github.com/red-data-tools/unicode_plot.rb -require_relative 'processing' +require_relative '../aggregation' require 'unicode_plot' # 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) series = data.series # `uplot count` if count - series = Processing.count_values(series[0], reverse: reverse) + series = YouPlot::Aggregation.count_values(series[0], reverse: reverse) params.title = headers[0] if headers end if series.size == 1 diff --git a/sig/youplot/aggregation.rbs b/sig/youplot/aggregation.rbs new file mode 100644 index 0000000..f328485 --- /dev/null +++ b/sig/youplot/aggregation.rbs @@ -0,0 +1,5 @@ +module YouPlot + module Aggregation + def self.count_values: (Enumerable[untyped], ?tally: bool, ?reverse: bool) -> Array[Array[untyped]] + end +end diff --git a/sig/youplot/backends/processing.rbs b/sig/youplot/backends/processing.rbs deleted file mode 100644 index 9830361..0000000 --- a/sig/youplot/backends/processing.rbs +++ /dev/null @@ -1,7 +0,0 @@ -module YouPlot - module Backends - module Processing - def count_values: ([], ?tally: true, ?reverse: false) -> Array[Array[untyped]] - end - end -end diff --git a/test/youplot/backends/processing_test.rb b/test/youplot/aggregation_test.rb similarity index 79% rename from test/youplot/backends/processing_test.rb rename to test/youplot/aggregation_test.rb index 417afa4..5ddc844 100644 --- a/test/youplot/backends/processing_test.rb +++ b/test/youplot/aggregation_test.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -require_relative '../../test_helper' +require_relative '../test_helper' -class ProcessingTest < Test::Unit::TestCase +class AggregationTest < Test::Unit::TestCase test :count_values do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%i[a b c], [3, 2, 1]], @m.count_values(%i[a a a b b c])) assert_equal([%i[c b a], [3, 2, 1]], @m.count_values(%i[a b b c c c])) end test :count_values_natural_sort_integer_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[19 187 1765], [1, 1, 1]], @m.count_values(%w[1765 187 19])) end test :count_values_natural_sort_alnum_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[a1 a2 a10], [1, 1, 1]], @m.count_values(%w[a10 a2 a1])) end test :count_values_natural_sort_negative_numeric_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[-20 -10 10], [1, 1, 1]], @m.count_values(%w[-10 10 -20])) end test :count_values_natural_sort_chr_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal( [%w[chr1 chr2 chr10 chr11 chr12], [1, 1, 1, 1, 1]], @m.count_values(%w[chr12 chr1 chr11 chr10 chr2]) @@ -33,27 +33,27 @@ class ProcessingTest < Test::Unit::TestCase end test :count_values_natural_sort_text_only_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[chrM chrX chrY], [1, 1, 1]], @m.count_values(%w[chrY chrX chrM])) end test :count_values_natural_sort_leading_zeros do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[01 1 2], [1, 1, 1]], @m.count_values(%w[2 1 01])) end test :count_values_natural_sort_mixed_numeric_and_text do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[2 10 abc], [1, 1, 1]], @m.count_values(%w[abc 10 2])) end test :count_values_natural_sort_numeric_and_mixed_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[2 2a 10], [1, 1, 1]], @m.count_values(%w[10 2a 2])) end test :count_values_natural_sort_ipv4_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal( [%w[192.168.0.2 192.168.0.10 192.168.1.1], [1, 1, 1]], @m.count_values(%w[192.168.1.1 192.168.0.10 192.168.0.2]) @@ -61,7 +61,7 @@ class ProcessingTest < Test::Unit::TestCase end test :count_values_natural_sort_version_labels do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal( [%w[1.2.3 1.2.10 1.10.0], [1, 1, 1]], @m.count_values(%w[1.10.0 1.2.10 1.2.3]) @@ -69,7 +69,7 @@ class ProcessingTest < Test::Unit::TestCase end test :count_values_mixed_counts_with_ties do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation # "a" appears 3 times (top), then "chr1" and "chr10" tie at 1 each assert_equal( [%w[a chr1 chr10], [3, 1, 1]], @@ -78,12 +78,12 @@ class ProcessingTest < Test::Unit::TestCase end test :count_values_reverse_preserves_semantics do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%w[1765 187 19], [1, 1, 1]], @m.count_values(%w[1765 187 19], reverse: true)) end test :count_values_non_tally do - @m = YouPlot::Backends::Processing + @m = YouPlot::Aggregation assert_equal([%i[a b c], [3, 2, 1]], @m.count_values(%i[a a a b b c], tally: false)) assert_equal([%i[c b a], [3, 2, 1]], @m.count_values(%i[a b b c c c], tally: false)) end