tuple 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +32 -0
- data/ext/extconf.rb +2 -0
- data/ext/tuple.c +235 -0
- data/test/test_helper.rb +22 -0
- data/test/tuple_test.rb +96 -0
- metadata +62 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Justin Balthrop
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
= Tuple
|
2
|
+
|
3
|
+
Tuple provides fast, binary-sortable serialization for arrays of simple Ruby types. This
|
4
|
+
means you do not have to deserialize your tuples to store them. This leads to significant
|
5
|
+
performance benifits when using Tuples as keys for a BTree.
|
6
|
+
|
7
|
+
A Tuple is just an Array of any number of simple Ruby types. The following types are
|
8
|
+
supported (listed in ascending sort order):
|
9
|
+
|
10
|
+
1. NilClass
|
11
|
+
2. FalseClass
|
12
|
+
3. Integer (Fixnum or Bignum)
|
13
|
+
4. String
|
14
|
+
5. Symbol
|
15
|
+
6. True
|
16
|
+
|
17
|
+
== Usage:
|
18
|
+
|
19
|
+
require 'tuple'
|
20
|
+
|
21
|
+
data = Tuple.dump([1, -43, :foo, "bar", true, false, nil])
|
22
|
+
=> "\000\000\020\000\000\000\000\001..."
|
23
|
+
Tuple.load(data)
|
24
|
+
=> [1, -43, :foo, "bar", true, false, nil]
|
25
|
+
|
26
|
+
== Install:
|
27
|
+
|
28
|
+
sudo gem install ninjudd-tuple -s http://gems.github.com
|
29
|
+
|
30
|
+
== License:
|
31
|
+
|
32
|
+
Copyright (c) 2009 Justin Balthrop, Geni.com; Published under The MIT License, see LICENSE
|
data/ext/extconf.rb
ADDED
data/ext/tuple.c
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
#include "ruby.h"
|
2
|
+
#include <netinet/in.h>
|
3
|
+
|
4
|
+
VALUE mTuple;
|
5
|
+
VALUE rb_cDate;
|
6
|
+
|
7
|
+
#define TRUE_SORT 255 // TrueClass
|
8
|
+
#define TUPLE_SORT 192 // Array
|
9
|
+
#define TUPLE_END 191 // For nested tuples
|
10
|
+
#define TIME_SORT 128 // Time
|
11
|
+
#define SYM_SORT 64 // Symbol
|
12
|
+
#define STR_SORT 32 // String
|
13
|
+
#define INTP_SORT 16 // Integer (Positive)
|
14
|
+
#define INTN_SORT 8 // Integer (Negative)
|
15
|
+
#define FALSE_SORT 1 // FalseClass
|
16
|
+
#define NIL_SORT 0 // NilClass
|
17
|
+
|
18
|
+
#define BDIGITS(x) ((BDIGIT*)RBIGNUM(x)->digits)
|
19
|
+
|
20
|
+
static void null_pad(VALUE data, int len) {
|
21
|
+
static u_int8_t null = 0;
|
22
|
+
|
23
|
+
// Pad with null bytes so subsequent fields will be aligned.
|
24
|
+
while (len % 4 != 0) {
|
25
|
+
rb_str_cat(data, (char*)&null, 1);
|
26
|
+
len++;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
u_int32_t split64(int64_t num, int word) {
|
32
|
+
u_int32_t *split = (u_int32_t*)(void*)#
|
33
|
+
|
34
|
+
static int i = 1;
|
35
|
+
if (*(char *)&i == 1) word = word ? 1: 0;
|
36
|
+
else word = word ? 0: 1;
|
37
|
+
|
38
|
+
return split[word];
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
/*
|
44
|
+
* call-seq:
|
45
|
+
* Tuple.dump(tuple) -> string
|
46
|
+
*
|
47
|
+
* Dumps an array of simple Ruby types into a string of binary data.
|
48
|
+
*
|
49
|
+
*/
|
50
|
+
static VALUE tuple_dump(VALUE self, VALUE tuple) {
|
51
|
+
VALUE data = rb_str_new2("");
|
52
|
+
VALUE item;
|
53
|
+
int i, j, len, sign;
|
54
|
+
u_int8_t header[4];
|
55
|
+
u_int32_t digit;
|
56
|
+
int64_t fixnum;
|
57
|
+
BDIGIT *digits;
|
58
|
+
|
59
|
+
|
60
|
+
if (TYPE(tuple) != T_ARRAY) tuple = rb_ary_new4(1, &tuple);
|
61
|
+
|
62
|
+
for (i = 0; i < RARRAY(tuple)->len; i++) {
|
63
|
+
item = RARRAY(tuple)->ptr[i];
|
64
|
+
header[0] = header[1] = header[2] = header[3] = 0;
|
65
|
+
if (FIXNUM_P(item)) {
|
66
|
+
fixnum = FIX2LONG(item);
|
67
|
+
sign = (fixnum >= 0);
|
68
|
+
if (!sign) fixnum = -fixnum;
|
69
|
+
len = fixnum > UINT_MAX ? 2 : 1;
|
70
|
+
header[2] = sign ? INTP_SORT : INTN_SORT;
|
71
|
+
header[3] = sign ? len : UCHAR_MAX - len;
|
72
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
73
|
+
|
74
|
+
if (len == 2) {
|
75
|
+
digit = split64(fixnum, 1);
|
76
|
+
digit = htonl(sign ? digit : UINT_MAX - digit);
|
77
|
+
rb_str_cat(data, (char*)&digit, sizeof(digit));
|
78
|
+
}
|
79
|
+
digit = split64(fixnum, 0);
|
80
|
+
digit = htonl(sign ? digit : UINT_MAX - digit);
|
81
|
+
rb_str_cat(data, (char*)&digit, sizeof(digit));
|
82
|
+
} else if (TYPE(item) == T_BIGNUM) {
|
83
|
+
sign = RBIGNUM(item)->sign;
|
84
|
+
len = RBIGNUM(item)->len;
|
85
|
+
header[2] = sign ? INTP_SORT : INTN_SORT;
|
86
|
+
header[3] = sign ? len : UCHAR_MAX - len;
|
87
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
88
|
+
|
89
|
+
digits = BDIGITS(item);
|
90
|
+
for (j = len-1; j >= 0; j--) {
|
91
|
+
digit = htonl(sign ? digits[j] : (UINT_MAX - digits[j]));
|
92
|
+
rb_str_cat(data, (char*)&digit, sizeof(digit));
|
93
|
+
}
|
94
|
+
} else if (SYMBOL_P(item) || TYPE(item) == T_STRING) {
|
95
|
+
if (SYMBOL_P(item)) {
|
96
|
+
header[2] = SYM_SORT;
|
97
|
+
item = rb_funcall(item, rb_intern("to_s"), 0);
|
98
|
+
} else {
|
99
|
+
header[2] = STR_SORT;
|
100
|
+
}
|
101
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
102
|
+
len = RSTRING_LEN(item);
|
103
|
+
rb_str_cat(data, RSTRING_PTR(item), len);
|
104
|
+
|
105
|
+
null_pad(data, len);
|
106
|
+
} else if (rb_obj_class(item) == rb_cTime || rb_obj_class(item) == rb_cDate) {
|
107
|
+
header[2] = TIME_SORT;
|
108
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
109
|
+
|
110
|
+
if (rb_obj_class(item) == rb_cTime) {
|
111
|
+
item = rb_funcall(item, rb_intern("getgm"), 0);
|
112
|
+
item = rb_funcall(item, rb_intern("strftime"), 1, rb_str_new2("%Y/%m/%d %H:%M:%S +0000"));
|
113
|
+
} else {
|
114
|
+
item = rb_funcall(item, rb_intern("strftime"), 1, rb_str_new2("%Y/%m/%d"));
|
115
|
+
}
|
116
|
+
len = RSTRING_LEN(item);
|
117
|
+
rb_str_cat(data, RSTRING_PTR(item), len);
|
118
|
+
|
119
|
+
null_pad(data, len);
|
120
|
+
} else if (TYPE(item) == T_ARRAY) {
|
121
|
+
header[2] = TUPLE_SORT;
|
122
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
123
|
+
|
124
|
+
rb_str_concat(data, tuple_dump(mTuple, item));
|
125
|
+
|
126
|
+
header[2] = TUPLE_END;
|
127
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
128
|
+
} else {
|
129
|
+
if (item == Qnil) header[2] = NIL_SORT;
|
130
|
+
else if (item == Qtrue) header[2] = TRUE_SORT;
|
131
|
+
else if (item == Qfalse) header[2] = FALSE_SORT;
|
132
|
+
else rb_raise(rb_eTypeError, "invalid type %s in tuple", rb_obj_classname(item));
|
133
|
+
|
134
|
+
rb_str_cat(data, (char*)&header, sizeof(header));
|
135
|
+
}
|
136
|
+
}
|
137
|
+
return data;
|
138
|
+
}
|
139
|
+
|
140
|
+
static VALUE empty_bignum(int sign, int len) {
|
141
|
+
/* Create an empty bignum with the right number of digits. */
|
142
|
+
NEWOBJ(num, struct RBignum);
|
143
|
+
OBJSETUP(num, rb_cBignum, T_BIGNUM);
|
144
|
+
num->sign = sign ? 1 : 0;
|
145
|
+
num->len = len;
|
146
|
+
num->digits = ALLOC_N(BDIGIT, len);
|
147
|
+
|
148
|
+
return (VALUE)num;
|
149
|
+
}
|
150
|
+
|
151
|
+
static VALUE tuple_parse(void **data, int data_len) {
|
152
|
+
VALUE tuple = rb_ary_new();
|
153
|
+
VALUE item;
|
154
|
+
void* ptr = *data; *data = &ptr;
|
155
|
+
void* end = ptr + data_len;
|
156
|
+
int i, len, sign;
|
157
|
+
u_int8_t header[4];
|
158
|
+
u_int32_t digit;
|
159
|
+
BDIGIT *digits;
|
160
|
+
|
161
|
+
while (ptr < end) {
|
162
|
+
memcpy(header, ptr, 4);
|
163
|
+
ptr += 4;
|
164
|
+
|
165
|
+
switch(header[2]) {
|
166
|
+
case TRUE_SORT: rb_ary_push(tuple, Qtrue); break;
|
167
|
+
case FALSE_SORT: rb_ary_push(tuple, Qfalse); break;
|
168
|
+
case NIL_SORT: rb_ary_push(tuple, Qnil); break;
|
169
|
+
case INTP_SORT:
|
170
|
+
case INTN_SORT:
|
171
|
+
sign = (header[2] == INTP_SORT);
|
172
|
+
len = sign ? header[3] : (UCHAR_MAX - header[3]);
|
173
|
+
|
174
|
+
item = empty_bignum(sign, len);
|
175
|
+
digits = BDIGITS(item);
|
176
|
+
for (i = len-1; i >= 0; i--) {
|
177
|
+
digit = ntohl(*(u_int32_t*)ptr);
|
178
|
+
digits[i] = sign ? digit : UINT_MAX - digit;
|
179
|
+
ptr += 4;
|
180
|
+
}
|
181
|
+
rb_ary_push(tuple, item);
|
182
|
+
break;
|
183
|
+
case STR_SORT:
|
184
|
+
case SYM_SORT:
|
185
|
+
item = rb_str_new2(ptr);
|
186
|
+
len = RSTRING_LEN(item);
|
187
|
+
if (header[2] == SYM_SORT) item = rb_funcall(item, rb_intern("to_sym"), 0);
|
188
|
+
rb_ary_push(tuple, item);
|
189
|
+
while (len % 4 != 0) len++; ptr += len;
|
190
|
+
break;
|
191
|
+
case TIME_SORT:
|
192
|
+
item = rb_str_new2(ptr);
|
193
|
+
len = RSTRING_LEN(item);
|
194
|
+
if (len == 10) item = rb_funcall(rb_cDate, rb_intern("parse"), 1, item);
|
195
|
+
else item = rb_funcall(rb_cTime, rb_intern("parse"), 1, item);
|
196
|
+
rb_ary_push(tuple, item);
|
197
|
+
while (len % 4 != 0) len++; ptr += len;
|
198
|
+
break;
|
199
|
+
case TUPLE_SORT:
|
200
|
+
item = tuple_parse(&ptr, end - ptr);
|
201
|
+
rb_ary_push(tuple, item);
|
202
|
+
break;
|
203
|
+
case TUPLE_END:
|
204
|
+
return tuple;
|
205
|
+
default:
|
206
|
+
rb_raise(rb_eTypeError, "invalid type code %d in tuple", header[2]);
|
207
|
+
break;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
return tuple;
|
211
|
+
}
|
212
|
+
|
213
|
+
/*
|
214
|
+
* call-seq:
|
215
|
+
* Tuple.load(string) -> tuple
|
216
|
+
*
|
217
|
+
* Reads in a previously dumped tuple from a string of binary data.
|
218
|
+
*
|
219
|
+
*/
|
220
|
+
static VALUE tuple_load(VALUE self, VALUE data) {
|
221
|
+
data = StringValue(data);
|
222
|
+
void* ptr = RSTRING_PTR(data);
|
223
|
+
return tuple_parse(&ptr, RSTRING_LEN(data));
|
224
|
+
}
|
225
|
+
|
226
|
+
VALUE mTuple;
|
227
|
+
void Init_tuple() {
|
228
|
+
rb_require("time");
|
229
|
+
rb_require("date");
|
230
|
+
rb_cDate = rb_const_get(rb_cObject, rb_intern("Date"));
|
231
|
+
|
232
|
+
mTuple = rb_define_module("Tuple");
|
233
|
+
rb_define_module_function(mTuple, "dump", tuple_dump, 1);
|
234
|
+
rb_define_module_function(mTuple, "load", tuple_load, 1);
|
235
|
+
}
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../ext')
|
7
|
+
require 'tuple'
|
8
|
+
|
9
|
+
if not [].respond_to?(:shuffle)
|
10
|
+
class Array
|
11
|
+
def shuffle
|
12
|
+
t_self = self.dup
|
13
|
+
t_size = self.size
|
14
|
+
result=[]
|
15
|
+
t_size.times { result << t_self.slice!(rand(t_self.size)) }
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Test::Unit::TestCase
|
22
|
+
end
|
data/test/tuple_test.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Time
|
4
|
+
def ==(other)
|
5
|
+
# Ignore microseconds for testing.
|
6
|
+
to_i == other.to_i
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class TupleTest < Test::Unit::TestCase
|
11
|
+
should "dump and load arrays of simple types" do
|
12
|
+
t = [1, true, :foo, "foo", -1001, false, nil, Time.now, Date.today - 7, [:foo, 1, 4, nil]]
|
13
|
+
assert_equal t, Tuple.load(Tuple.dump(t))
|
14
|
+
end
|
15
|
+
|
16
|
+
should "dump, load, and sort fixnums and bignums" do
|
17
|
+
t = [2**64, 2**38, 2**32, 2**32 - 1, 2**31, 2**31 - 1, 1, 0]
|
18
|
+
t = t + t.reverse.collect {|n| -n}
|
19
|
+
assert_equal t, Tuple.load(Tuple.dump(t))
|
20
|
+
assert_equal t.reverse, t.sort_by {|i| Tuple.dump(i)}
|
21
|
+
end
|
22
|
+
|
23
|
+
should "convert single value into array" do
|
24
|
+
assert_equal [1], Tuple.load(Tuple.dump(1))
|
25
|
+
end
|
26
|
+
|
27
|
+
should "dump times consistently" do
|
28
|
+
t = '2009-10-15 1:23:45 PM'
|
29
|
+
tuple = Tuple.dump(Time.parse(t))
|
30
|
+
100000.times do
|
31
|
+
assert_equal tuple, Tuple.dump(Time.parse(t))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
should "sort tuples using binary" do
|
36
|
+
now = Time.now.getgm
|
37
|
+
today = Date.parse(now.to_s)
|
38
|
+
|
39
|
+
tuples = [
|
40
|
+
[1, "foo"],
|
41
|
+
[1, true],
|
42
|
+
[2],
|
43
|
+
[1],
|
44
|
+
[nil],
|
45
|
+
[true],
|
46
|
+
[:foo, -1],
|
47
|
+
[:foo, -2**64],
|
48
|
+
[:foo, 2**64],
|
49
|
+
[1, "foo", 7, nil, false, true],
|
50
|
+
[1, "foo", 7, nil, false, false],
|
51
|
+
["charles", "atlas"],
|
52
|
+
["charles", "atlas", "shrugged"],
|
53
|
+
["charles", "atlantic"],
|
54
|
+
["charles", "atlas jr."],
|
55
|
+
["charles", "atlas", "world's", "strongest", "man"],
|
56
|
+
["charles", "atlas", 5],
|
57
|
+
[now, "foo"],
|
58
|
+
[now, "bar"],
|
59
|
+
[now - 24 * 60 * 60],
|
60
|
+
[today + 1],
|
61
|
+
[today - 1],
|
62
|
+
[today],
|
63
|
+
]
|
64
|
+
|
65
|
+
expected = [
|
66
|
+
[nil],
|
67
|
+
[1],
|
68
|
+
[1, "foo"],
|
69
|
+
[1, "foo", 7, nil, false, false],
|
70
|
+
[1, "foo", 7, nil, false, true],
|
71
|
+
[1, true],
|
72
|
+
[2],
|
73
|
+
["charles", "atlantic"],
|
74
|
+
["charles", "atlas"],
|
75
|
+
["charles", "atlas", 5],
|
76
|
+
["charles", "atlas", "shrugged"],
|
77
|
+
["charles", "atlas", "world's", "strongest", "man"],
|
78
|
+
["charles", "atlas jr."],
|
79
|
+
[:foo, -18446744073709551616],
|
80
|
+
[:foo, -1],
|
81
|
+
[:foo, 18446744073709551616],
|
82
|
+
[today - 1],
|
83
|
+
[now - 24 * 60 * 60],
|
84
|
+
[today],
|
85
|
+
[now, "bar"],
|
86
|
+
[now, "foo"],
|
87
|
+
[today + 1],
|
88
|
+
[true]
|
89
|
+
]
|
90
|
+
assert_equal expected, tuples.sort_by {|t| Tuple.dump(t)}
|
91
|
+
|
92
|
+
100.times do
|
93
|
+
assert_equal expected, tuples.shuffle.sort_by {|t| Tuple.dump(t)}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tuple
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Balthrop
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-20 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Fast, binary-sortable serialization for arrays of simple Ruby types.
|
17
|
+
email: code@justinbalthrop.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions:
|
21
|
+
- ext/extconf.rb
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- README.rdoc
|
27
|
+
- ext/extconf.rb
|
28
|
+
- ext/tuple.c
|
29
|
+
- test/test_helper.rb
|
30
|
+
- test/tuple_test.rb
|
31
|
+
- LICENSE
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://github.com/ninjudd/tuple
|
34
|
+
licenses: []
|
35
|
+
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options:
|
38
|
+
- --charset=UTF-8
|
39
|
+
require_paths:
|
40
|
+
- ext
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.3.5
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Tuple serialization functions.
|
60
|
+
test_files:
|
61
|
+
- test/test_helper.rb
|
62
|
+
- test/tuple_test.rb
|