diff --git a/.gitignore b/.gitignore index a696c7f..c86cfba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .ruby-version Makefile -*.bundle *.o *.log tmp/ + +/target/ +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml index 0af306e..5d20552 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,10 @@ sudo: false language: ruby script: script/test before_install: - - bundle --version 2>/dev/null | grep -q '1.7.6' && gem install bundler -v 1.11.2 || true + - gem install bundler -v 1.16.1 + - curl https://sh.rustup.rs -sSf | sh -s -- -y + - export PATH=$HOME/.cargo/bin:$PATH rvm: - - "1.8.7" - - "1.9.3" - - "2.0" - "2.1" - "2.2" - "2.3" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..431c448 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "version_sorter" +version = "0.1.0" +authors = ["Ashe Connor "] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +helix = "0.7" diff --git a/Gemfile b/Gemfile index 31caac8..b99630a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,6 @@ source "https://rubygems.org" gem "rake" -gem "rake-compiler" gem "test-unit", "~> 2.0.4" gemspec diff --git a/Gemfile.lock b/Gemfile.lock index e9c544a..2195788 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,24 +1,29 @@ PATH remote: . specs: - version_sorter (2.2.1) + version_sorter (2.2.1-x86_64-darwin-17) + helix_runtime (~> 0.7) GEM remote: https://rubygems.org/ specs: + helix_runtime (0.7.2) + rake (>= 10.0) + thor (~> 0.19.4) + tomlrb (~> 1.2.4) rake (10.4.2) - rake-compiler (0.9.5) - rake test-unit (2.0.9) + thor (0.19.4) + tomlrb (1.2.6) PLATFORMS ruby DEPENDENCIES + helix_runtime (~> 0.7) rake - rake-compiler test-unit (~> 2.0.4) version_sorter! BUNDLED WITH - 1.15.2 + 1.16.0 diff --git a/Rakefile b/Rakefile index cd92684..cdba671 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,5 @@ require 'rake/testtask' -require 'rake/extensiontask' +import 'lib/tasks/helix_runtime.rake' task :default => :test @@ -8,12 +8,7 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList['test/*test.rb'] end -Rake::ExtensionTask.new('version_sorter') - -desc "Compile the native extension" -task :test => :compile - desc "Run the benchmark" -task :benchmark => :compile do +task :benchmark do sh 'script/benchmark' end diff --git a/ext/version_sorter/extconf.rb b/ext/version_sorter/extconf.rb deleted file mode 100644 index 41c8011..0000000 --- a/ext/version_sorter/extconf.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'mkmf' - -create_makefile("version_sorter") diff --git a/ext/version_sorter/version_sorter.c b/ext/version_sorter/version_sorter.c deleted file mode 100644 index 37c000e..0000000 --- a/ext/version_sorter/version_sorter.c +++ /dev/null @@ -1,251 +0,0 @@ -/* - * version_sorter.c - * version_sorter - * - * Created by K. Adam Christensen on 10/10/09. - * Copyright 2009. All rights reserved. - * - */ - -#include -#include -#include -#include -#include -#include - -#define min(a, b) ((a) < (b) ? (a) : (b)) -typedef int compare_callback_t(const void *, const void *); - -struct version_number { - const char *original; - VALUE rb_version; - uint64_t num_flags; - int32_t size; - union version_comp { - uint32_t number; - struct strchunk { - uint16_t offset; - uint16_t len; - } string; - } comp[1]; -}; - -static int -strchunk_cmp(const char *original_a, const struct strchunk *a, - const char *original_b, const struct strchunk *b) -{ - size_t len = min(a->len, b->len); - int cmp = memcmp(original_a + a->offset, original_b + b->offset, len); - return cmp ? cmp : (int)(a->len - b->len); -} - -static int -compare_version_number(const struct version_number *a, - const struct version_number *b) -{ - int n, max_n = min(a->size, b->size); - - for (n = 0; n < max_n; ++n) { - int num_a = (a->num_flags & (1ull << n)) != 0; - int num_b = (b->num_flags & (1ull << n)) != 0; - - if (num_a == num_b) { - const union version_comp *ca = &a->comp[n]; - const union version_comp *cb = &b->comp[n]; - int cmp = 0; - - if (num_a) { - cmp = (int)ca->number - (int)cb->number; - } else { - cmp = strchunk_cmp( - a->original, &ca->string, - b->original, &cb->string); - } - - if (cmp) return cmp; - } else { - return num_a ? 1 : -1; - } - } - - if (a->size < b->size) - return (b->num_flags & (1ull << n)) ? -1 : 1; - - if (a->size > b->size) - return (a->num_flags & (1ull << n)) ? 1 : -1; - - return 0; -} - -static int -version_compare_cb(const void *a, const void *b) -{ - return compare_version_number( - (*(const struct version_number **)a), - (*(const struct version_number **)b)); -} - -static int -version_compare_cb_r(const void *a, const void *b) -{ - return -compare_version_number( - (*(const struct version_number **)a), - (*(const struct version_number **)b)); -} - -static struct version_number * -grow_version_number(struct version_number *version, int new_size) -{ - return xrealloc(version, - (sizeof(struct version_number) + - sizeof(union version_comp) * new_size)); -} - -static struct version_number * -parse_version_number(const char *string) -{ - struct version_number *version = NULL; - uint64_t num_flags = 0x0; - uint16_t offset; - int comp_n = 0, comp_alloc = 4; - - version = grow_version_number(version, comp_alloc); - - for (offset = 0; string[offset] && comp_n < 64;) { - if (comp_n >= comp_alloc) { - comp_alloc += 4; - version = grow_version_number(version, comp_alloc); - } - - if (isdigit(string[offset])) { - uint32_t number = 0; - uint16_t start = offset; - int overflown = 0; - - while (isdigit(string[offset])) { - if (!overflown) { - uint32_t old_number = number; - number = (10 * number) + (string[offset] - '0'); - if (number < old_number) overflown = 1; - } - - offset++; - } - - if (overflown) { - version->comp[comp_n].string.offset = start; - version->comp[comp_n].string.len = offset - start; - } else { - version->comp[comp_n].number = number; - num_flags |= (1 << comp_n); - } - comp_n++; - continue; - } - - if (string[offset] == '-' || isalpha(string[offset])) { - uint16_t start = offset; - - if (string[offset] == '-') - offset++; - - while (isalpha(string[offset])) - offset++; - - version->comp[comp_n].string.offset = start; - version->comp[comp_n].string.len = offset - start; - comp_n++; - continue; - } - - offset++; - } - - version->original = string; - version->num_flags = num_flags; - version->size = comp_n; - - return version; -} - -static VALUE -rb_version_sort_1(VALUE rb_self, VALUE rb_version_array, compare_callback_t cmp) -{ - struct version_number **versions; - long length, i; - VALUE *rb_version_ptr; - - Check_Type(rb_version_array, T_ARRAY); - - length = RARRAY_LEN(rb_version_array); - if (!length) - return rb_ary_new(); - - versions = xcalloc(length, sizeof(struct version_number *)); - - for (i = 0; i < length; ++i) { - VALUE rb_version, rb_version_string; - - rb_version = rb_ary_entry(rb_version_array, i); - if (rb_block_given_p()) - rb_version_string = rb_yield(rb_version); - else - rb_version_string = rb_version; - - versions[i] = parse_version_number(StringValueCStr(rb_version_string)); - versions[i]->rb_version = rb_version; - } - - qsort(versions, length, sizeof(struct version_number *), cmp); - rb_version_ptr = RARRAY_PTR(rb_version_array); - - for (i = 0; i < length; ++i) { - rb_version_ptr[i] = versions[i]->rb_version; - xfree(versions[i]); - } - xfree(versions); - return rb_version_array; -} - -static VALUE -rb_version_sort(VALUE rb_self, VALUE rb_versions) -{ - return rb_version_sort_1(rb_self, rb_ary_dup(rb_versions), version_compare_cb); -} - -static VALUE -rb_version_sort_r(VALUE rb_self, VALUE rb_versions) -{ - return rb_version_sort_1(rb_self, rb_ary_dup(rb_versions), version_compare_cb_r); -} - -static VALUE -rb_version_sort_bang(VALUE rb_self, VALUE rb_versions) -{ - return rb_version_sort_1(rb_self, rb_versions, version_compare_cb); -} - -static VALUE -rb_version_sort_r_bang(VALUE rb_self, VALUE rb_versions) -{ - return rb_version_sort_1(rb_self, rb_versions, version_compare_cb_r); -} - -static VALUE -rb_version_compare(VALUE rb_self, VALUE rb_version_a, VALUE rb_version_b) -{ - struct version_number *version_a = parse_version_number(StringValueCStr(rb_version_a)); - struct version_number *version_b = parse_version_number(StringValueCStr(rb_version_b)); - return INT2NUM(version_compare_cb(&version_a, &version_b)); -} - -void Init_version_sorter(void) -{ - VALUE rb_mVersionSorter = rb_define_module("VersionSorter"); - rb_define_module_function(rb_mVersionSorter, "sort", rb_version_sort, 1); - rb_define_module_function(rb_mVersionSorter, "rsort", rb_version_sort_r, 1); - rb_define_module_function(rb_mVersionSorter, "sort!", rb_version_sort_bang, 1); - rb_define_module_function(rb_mVersionSorter, "rsort!", rb_version_sort_r_bang, 1); - rb_define_module_function(rb_mVersionSorter, "compare", rb_version_compare, 2); -} diff --git a/lib/tasks/helix_runtime.rake b/lib/tasks/helix_runtime.rake new file mode 100644 index 0000000..6734263 --- /dev/null +++ b/lib/tasks/helix_runtime.rake @@ -0,0 +1,6 @@ +require 'helix_runtime/build_task' + +HelixRuntime::BuildTask.new + +task :default => :build +task :test => :build diff --git a/lib/version_sorter.rb b/lib/version_sorter.rb new file mode 100644 index 0000000..37400cf --- /dev/null +++ b/lib/version_sorter.rb @@ -0,0 +1,48 @@ +require "helix_runtime" + +begin + require "version_sorter/native" +rescue LoadError + warn "Unable to lead version_sorter/native. Please run `rake build`" +end + +class VersionSorter + def self.sort(versions, &blk) + int_sort_with(versions, :int_sort, &blk) + end + + def self.rsort(versions, &blk) + int_sort_with(versions, :int_rsort, &blk) + end + + def self.int_sort_with(versions, method, &blk) + bases = if blk + versions.map(&blk) + else + versions + end + send(method, bases).map { |i| versions[i] } + end + + def self.sort!(versions, &blk) + int_sort_with!(versions, :int_sort, &blk) + end + + def self.rsort!(versions, &blk) + int_sort_with!(versions, :int_rsort, &blk) + end + + def self.int_sort_with!(versions, method, &blk) + bases = if blk + versions.map(&blk) + else + versions + end + indices = send(method, bases) + origs = versions.dup + indices.each.with_index do |ix, i| + versions[i] = origs[ix] + end + versions + end +end diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c14de6b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,150 @@ +#[macro_use] +extern crate helix; + +use std::cmp::{self, Ordering}; + +ruby! { + class VersionSorter { + def int_sort(versions: Vec) -> Vec { + do_sort(versions, cmp_fwd) + } + + def int_rsort(versions: Vec) -> Vec { + do_sort(versions, cmp_bwd) + } + + def compare(a: String, b: String) -> i32 { + cmp_fwd(&parse(&a, 0), &parse(&b, 0)) as i32 + } + } +} + +struct VersionNumber<'a> { + i: usize, + comps: Vec>, +} + +enum VersionComponent<'a> { + String(&'a [u8]), + Number(u32), +} + +fn cmp_fwd(a: &VersionNumber, b: &VersionNumber) -> Ordering { + let max_n = cmp::min(a.comps.len(), b.comps.len()); + + for n in 0..max_n { + match (&a.comps[n], &b.comps[n]) { + (&VersionComponent::Number(a), &VersionComponent::Number(b)) => match a.cmp(&b) { + Ordering::Equal => (), + r => return r, + }, + (&VersionComponent::String(a), &VersionComponent::String(b)) => { + let alen = a.len(); + let blen = b.len(); + let sz = cmp::min(alen, blen); + let a = &a[..sz]; + let b = &b[..sz]; + match a.cmp(&b) { + Ordering::Equal => { + if alen < blen { + return Ordering::Less; + } else if alen == blen { + () + } else { + return Ordering::Greater; + } + }, + r => return r, + } + } + (&VersionComponent::Number(_), _) => return Ordering::Greater, + _ => return Ordering::Less, + } + } + + if a.comps.len() < b.comps.len() { + match &b.comps[max_n] { + &VersionComponent::Number(_) => Ordering::Less, + _ => Ordering::Greater, + } + } else if a.comps.len() > b.comps.len() { + match &a.comps[max_n] { + &VersionComponent::Number(_) => Ordering::Greater, + _ => Ordering::Less, + } + } else { + Ordering::Equal + } +} + +fn cmp_bwd(a: &VersionNumber, b: &VersionNumber) -> Ordering { + cmp_fwd(a, b).reverse() +} + +fn do_sort(versions: Vec, cmp: F) -> Vec +where + F: FnMut(&VersionNumber, &VersionNumber) -> Ordering, +{ + let len = versions.len(); + let mut vns = Vec::with_capacity(len); + for i in 0..len { + vns.push(parse(&versions[i], i)); + } + vns.sort_unstable_by(cmp); + + let mut sorted_versions = Vec::with_capacity(len); + for i in 0..len { + sorted_versions.push(vns[i].i as u32); + } + + sorted_versions +} + +fn parse(v: &str, i: usize) -> VersionNumber { + let v = v.as_bytes(); + let mut comps = vec![]; + + let mut offset = 0; + let len = v.len(); + while offset < len && comps.len() < 64 { + if (v[offset] as char).is_digit(10) { + let mut number: u32 = 0; + let start = offset; + let mut overflown = false; + + while offset < len && (v[offset] as char).is_digit(10) { + let old_number = number; + number = number + .wrapping_mul(10) + .wrapping_add((v[offset] - b'0') as u32); + if number < old_number { + overflown = true; + } + + offset += 1; + } + + if overflown { + comps.push(VersionComponent::String(&v[start..offset])); + } else { + comps.push(VersionComponent::Number(number)); + } + } else if v[offset] == b'-' || (v[offset] as char).is_alphabetic() { + let start = offset; + + if v[offset] == b'-' { + offset += 1; + } + + while offset < len && (v[offset] as char).is_alphabetic() { + offset += 1; + } + + comps.push(VersionComponent::String(&v[start..offset])); + } else { + offset += 1; + } + } + + VersionNumber { i: i, comps: comps } +} diff --git a/test/version_sorter_test.rb b/test/version_sorter_test.rb index 307704e..f6f724e 100644 --- a/test/version_sorter_test.rb +++ b/test/version_sorter_test.rb @@ -101,6 +101,12 @@ def test_compare assert_equal 0, VersionSorter.compare("12.0", "12.0") end + def test_i18n_issues + versions = ["i18n", "issues"] + sorted = VersionSorter.sort(versions) + assert_equal versions, sorted + end + def shuffle(array) array, result = array.dup, [] result << array.delete_at(rand(array.size)) until array.size.zero? diff --git a/version_sorter.gemspec b/version_sorter.gemspec index 48479d3..f284f4e 100644 --- a/version_sorter.gemspec +++ b/version_sorter.gemspec @@ -12,13 +12,10 @@ Gem::Specification.new do |s| s.summary = "Fast sorting of version strings" s.description = "VersionSorter is a C extension that does fast sorting of large sets of version strings." - s.files = Dir['{lib,ext}/**/*', 'LICENSE*'] - s.extensions << 'ext/version_sorter/extconf.rb' + s.files = Dir['lib/**/*', 'LICENSE*', 'Cargo.*', 'src/**/*'] - # include only files in version control - git_dir = File.expand_path('../.git', __FILE__) - dev_null = defined?(File::NULL) ? File::NULL : - RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw/ ? 'NUL' : '/dev/null' - git_files = `git --git-dir='#{git_dir}' ls-files -z 2>#{dev_null}`.split("\0") - s.files &= git_files if git_files.any? + s.platform = Gem::Platform::CURRENT + + s.add_development_dependency 'helix_runtime', '~> 0.7' + s.add_runtime_dependency 'helix_runtime', '~> 0.7' end