ccharts 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ccharts
4
+ # High-level chart API. A dataset built with {Chart.from_arrays},
5
+ # {Chart.from_json} or {Chart.from_csv} is immutable; the pie / histogram /
6
+ # sparkline / bar / stacked_bar / heatmap / boxplot renderers take their data
7
+ # directly (they have no OHLC dataset, exactly like the C ABI they wrap).
8
+ class Chart
9
+ # ------------------------------------------------------------------
10
+ # Building a dataset
11
+ # ------------------------------------------------------------------
12
+
13
+ # Build a dataset from four equal-length price columns and optional epoch
14
+ # seconds. `ts` may be nil (all timestamps unknown).
15
+ def self.from_arrays(open:, high:, low:, close:, ts: nil)
16
+ n = open.length
17
+ raise Error, "need at least one candle" if n == 0
18
+ unless [high, low, close].all? { |a| a.length == n }
19
+ raise Error, "open, high, low and close must have the same length"
20
+ end
21
+ if ts && ts.length != n
22
+ raise Error, "ts must have the same length as the price columns"
23
+ end
24
+
25
+ open_ptr = FFI.pack_doubles(open)
26
+ high_ptr = FFI.pack_doubles(high)
27
+ low_ptr = FFI.pack_doubles(low)
28
+ close_ptr = FFI.pack_doubles(close)
29
+ ts_ptr = ts ? FFI.pack_i64(ts) : Fiddle::NULL
30
+
31
+ out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
32
+ status = FFI::FROM_ARRAYS.call(open_ptr, high_ptr, low_ptr, close_ptr, ts_ptr, n, out)
33
+ _from_data_status(status, out)
34
+ end
35
+
36
+ # Build a dataset from the fixed-schema JSON document accepted by the
37
+ # C layer (an array of {ts, open, high, low, close} objects).
38
+ def self.from_json(text)
39
+ json = FFI.cstr(text)
40
+ out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
41
+ status = FFI::PARSE_JSON.call(json, out)
42
+ _from_data_status(status, out)
43
+ end
44
+
45
+ # Build a dataset from CSV rows of `open,high,low,close[,timestamp]`.
46
+ def self.from_csv(text, value_separator: ",", line_separator: "\n")
47
+ csv = FFI.cstr(text)
48
+ vs = value_separator.ord
49
+ ls = line_separator.ord
50
+ raise Error, "separators must not be NUL" if vs == 0 || ls == 0
51
+ out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
52
+ status = FFI::PARSE_CSV.call(csv, vs, ls, out)
53
+ _from_data_status(status, out)
54
+ end
55
+
56
+ def self._from_data_status(status, out)
57
+ Ccharts.raise_on(status)
58
+ addr = FFI.read_ptr_int(out, Fiddle::SIZEOF_VOIDP)
59
+ raise Error, "no dataset handle returned" if addr.zero?
60
+ _from_handle(Fiddle::Pointer.new(addr))
61
+ end
62
+
63
+ # ------------------------------------------------------------------
64
+ # Rendering an OHLC dataset (line / candle)
65
+ # ------------------------------------------------------------------
66
+
67
+ def line(width, height, settings = nil)
68
+ _render(FFI::LINE, width, height, settings || Settings::ChartSettings.new)
69
+ end
70
+
71
+ def candle(width, height, settings = nil)
72
+ _render(FFI::CANDLE, width, height, settings || Settings::ChartSettings.new)
73
+ end
74
+
75
+ # Number of candles in this dataset.
76
+ def size
77
+ FFI::DATA_LEN.call(@handle)
78
+ end
79
+ alias_method :length, :size
80
+
81
+ # ------------------------------------------------------------------
82
+ # Pie / histogram / sparkline / bar / stack / heat / box renderers
83
+ # ------------------------------------------------------------------
84
+
85
+ def self.pie(slices, width, height, settings = nil)
86
+ raise Error, "need at least one slice" if slices.empty?
87
+ settings ||= Settings::PieSettings.new
88
+
89
+ keep = []
90
+ rows = slices.map do |sl|
91
+ label = sl[:label] || sl["label"]
92
+ value = (sl[:value] || sl["value"] || 0.0).to_f
93
+ lp = FFI.cstr(label)
94
+ keep << lp
95
+ [[0, 8, "Q", lp.to_i], [8, 8, "d", value]] # label @0, value @8
96
+ end
97
+ slices_ptr = FFI.contiguous_structs(16, rows) # ccharts_pie_slice = 16 B
98
+ keep << slices_ptr
99
+
100
+ colors_ptr_addr = settings.colors_ptr(keep).to_i
101
+ center_ptr = settings.center_text_ptr(keep)
102
+ donut, show_legend, show_pct, slice_gap, inner_radius_ratio,
103
+ legend_format, start_angle, counter_clockwise = settings._scalars
104
+
105
+ # `_scalars` already returns the flags as C ints 0/1 — pass them straight
106
+ # through. Do NOT write `x ? 1 : 0` here: in Ruby `0` is truthy, so a
107
+ # zeroed flag would be coerced to 1 (pct/ccw always on → mirrored pie).
108
+ _read_string(
109
+ FFI::PIE, slices_ptr, slices.length, width, height,
110
+ donut, colors_ptr_addr, settings.colors_count,
111
+ show_legend, show_pct,
112
+ slice_gap, inner_radius_ratio, legend_format,
113
+ start_angle, counter_clockwise, center_ptr
114
+ )
115
+ end
116
+
117
+ def self.histogram(samples, width, height, settings = nil)
118
+ raise Error, "need at least one sample" if samples.empty?
119
+ settings ||= Settings::HistSettings.new
120
+ ptr = FFI.pack_doubles(samples)
121
+ s, keep = settings.to_ffi
122
+ _read_string(FFI::HIST, ptr, samples.length, width, height, s.to_ptr)
123
+ end
124
+
125
+ def self.sparkline(samples, width, height, settings = nil)
126
+ raise Error, "need at least one sample" if samples.empty?
127
+ settings ||= Settings::SparkSettings.new
128
+ ptr = FFI.pack_doubles(samples)
129
+ s, keep = settings.to_ffi
130
+ _read_string(FFI::SPARK, ptr, samples.length, width, height, s.to_ptr)
131
+ end
132
+
133
+ def self.bar(labels, values, width, height, settings = nil)
134
+ raise Error, "need at least one bar" if labels.empty?
135
+ raise Error, "labels and values must have the same length" unless labels.length == values.length
136
+ settings ||= Settings::BarSettings.new
137
+
138
+ keep = []
139
+ rows = labels.zip(values).map do |lbl, val|
140
+ lp = FFI.cstr(lbl)
141
+ keep << lp
142
+ [[0, 8, "Q", lp.to_i], [8, 8, "d", val.to_f]] # label @0, value @8
143
+ end
144
+ items_ptr = FFI.contiguous_structs(16, rows) # ccharts_bar_slice = 16 B
145
+ keep << items_ptr
146
+ s, skeep = settings.to_ffi
147
+ keep.concat(skeep)
148
+ _read_string(FFI::BAR, items_ptr, labels.length, width, height, s.to_ptr)
149
+ end
150
+
151
+ # series: array of {name:, values:} hashes sharing one category count.
152
+ def self.stacked_bar(series, width, height, settings = nil)
153
+ raise Error, "need at least one series" if series.empty?
154
+ cats = series[0][:values]&.length || series[0]["values"].length
155
+ raise Error, "series values must not be empty" if cats == 0
156
+ series.each do |sv|
157
+ vs = sv[:values] || sv["values"]
158
+ raise Error, "all series must have the same number of values" unless vs.length == cats
159
+ end
160
+ settings ||= Settings::StackSettings.new
161
+ settings.counts(series.length, cats)
162
+
163
+ keep = []
164
+ rows = series.map do |sv|
165
+ vs = sv[:values] || sv["values"]
166
+ name = sv[:name] || sv["name"]
167
+ np = FFI.cstr(name)
168
+ vp = FFI.pack_doubles(vs)
169
+ keep << np << vp
170
+ [[0, 8, "Q", np.to_i], [8, 8, "Q", vp.to_i]] # name @0, values @8
171
+ end
172
+ series_ptr = FFI.contiguous_structs(16, rows) # ccharts_stack_series = 16 B
173
+ keep << series_ptr
174
+ s, skeep = settings.to_ffi
175
+ keep.concat(skeep)
176
+ _read_string(FFI::STACK, series_ptr, series.length, width, height, s.to_ptr)
177
+ end
178
+
179
+ # values: a rows x cols matrix (array of equal-length arrays).
180
+ def self.heatmap(values, width, height, settings = nil)
181
+ raise Error, "need a non-empty value matrix" if values.empty?
182
+ cols = values[0].length
183
+ raise Error, "matrix columns must not be empty" if cols == 0
184
+ values.each { |row| raise Error, "all rows must have the same number of values" unless row.length == cols }
185
+ settings ||= Settings::HeatSettings.new
186
+
187
+ flat = values.flatten
188
+ flat_ptr = FFI.pack_doubles(flat)
189
+ s, keep = settings.to_ffi
190
+ _read_string(FFI::HEAT, flat_ptr, values.length, cols, width, height, s.to_ptr)
191
+ end
192
+
193
+ # series: array of {name:, samples:} hashes (per-category samples).
194
+ def self.boxplot(series, width, height, settings = nil)
195
+ raise Error, "need at least one category" if series.empty?
196
+ series.each do |c|
197
+ sm = c[:samples] || c["samples"]
198
+ raise Error, "every category must have at least one sample" if sm.empty?
199
+ end
200
+ settings ||= Settings::BoxSettings.new
201
+
202
+ keep = []
203
+ rows = series.map do |c|
204
+ sm = c[:samples] || c["samples"]
205
+ name = c[:name] || c["name"]
206
+ np = FFI.cstr(name)
207
+ sp = FFI.pack_doubles(sm)
208
+ keep << np << sp
209
+ [[0, 8, "Q", np.to_i], [8, 8, "Q", sp.to_i], [16, 4, "l", sm.length]] # name@0, samples@8, n@16 (24 B)
210
+ end
211
+ cats_ptr = FFI.contiguous_structs(24, rows) # ccharts_box_category = 24 B
212
+ keep << cats_ptr
213
+ s, skeep = settings.to_ffi
214
+ keep.concat(skeep)
215
+ _read_string(FFI::BOX, cats_ptr, series.length, width, height, s.to_ptr)
216
+ end
217
+
218
+ # ------------------------------------------------------------------
219
+ # Internals
220
+ # ------------------------------------------------------------------
221
+
222
+ def self._from_handle(handle)
223
+ obj = allocate
224
+ obj.instance_variable_set(:@handle, handle)
225
+ ObjectSpace.define_finalizer(obj, _finalizer(handle.to_i))
226
+ obj
227
+ end
228
+
229
+ def self._finalizer(addr)
230
+ proc { FFI::DATA_FREE.call(Fiddle::Pointer.new(addr)) }
231
+ end
232
+
233
+ def _render(fn, width, height, settings)
234
+ s, = settings.to_ffi
235
+ self.class._read_string(fn, @handle, width, height, s.to_ptr)
236
+ end
237
+
238
+ # Call a render function and copy the returned C string, releasing it.
239
+ def self._read_string(fn, *args)
240
+ out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)
241
+ out_len = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SIZE_T)
242
+ status = fn.call(*args, out, out_len)
243
+ Ccharts.raise_on(status)
244
+ addr = FFI.read_ptr_int(out, Fiddle::SIZEOF_VOIDP)
245
+ len = FFI.read_ptr_int(out_len, Fiddle::SIZEOF_SIZE_T)
246
+ s = Fiddle::Pointer.new(addr).to_s(len).force_encoding("UTF-8")
247
+ FFI::STRING_FREE.call(Fiddle::Pointer.new(addr))
248
+ s
249
+ end
250
+ private_class_method :_from_handle, :_from_data_status, :_finalizer
251
+ private :_render
252
+ end
253
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ccharts
4
+ # ANSI color escapes matching the C library's ccharts_color_index values
5
+ # (abi/ccharts_abi.h). These come straight from the library: a binding passes
6
+ # the escape string (or NULL for the library default) through the settings
7
+ # structs, so every binding resolves the same names to the same escapes.
8
+ module Color
9
+ ESCAPES = {
10
+ "black" => "\e[30m",
11
+ "red" => "\e[31m",
12
+ "green" => "\e[32m",
13
+ "yellow" => "\e[33m",
14
+ "blue" => "\e[34m",
15
+ "magenta" => "\e[35m",
16
+ "cyan" => "\e[36m",
17
+ "white" => "\e[37m",
18
+ "bright_black" => "\e[90m",
19
+ "bright_red" => "\e[91m",
20
+ "bright_green" => "\e[92m",
21
+ "bright_yellow"=> "\e[93m",
22
+ "bright_blue" => "\e[94m",
23
+ "bright_magenta"=> "\e[95m",
24
+ "bright_cyan" => "\e[96m",
25
+ "bright_white" => "\e[97m",
26
+ "reset" => "\e[0m",
27
+ }.freeze
28
+
29
+ # Resolve a color given either a name ("blue"), a Symbol (:blue), or a raw
30
+ # ANSI escape sequence (256-color/truecolor strings passed straight through).
31
+ # Returns the escape string, or nil if the value is nil/empty (library
32
+ # default). Unknown names fall back to the literal string so raw escapes
33
+ # keep working.
34
+ def self.resolve(value)
35
+ return nil if value.nil?
36
+ s = value.to_s
37
+ return nil if s.empty?
38
+ ESCAPES[s] || s
39
+ end
40
+
41
+ # The ANSI escape for a named color, or nil when unknown.
42
+ def self.[](name)
43
+ ESCAPES[name.to_s]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "fiddle/import"
5
+ require "rbconfig"
6
+
7
+ module Ccharts
8
+ # Raw Fiddle bindings to the vendored ccharts ABI (ext/ccharts/vendor).
9
+ #
10
+ # The extension (ccharts_ext.so) is built from ext/ccharts/vendor/ccharts_abi.c
11
+ # by mkmf — it is pure object code, so Fiddle just dlopens it and resolves
12
+ # the 20 ccharts_* symbols below. The struct layouts mirror abi/ccharts_abi.h
13
+ # exactly (field order + C alignment); a mismatch shows up as a conformance
14
+ # failure, not a crash.
15
+ module FFI
16
+ extend Fiddle::Importer
17
+
18
+ DLEXT = RbConfig::CONFIG["DLEXT"] # e.g. "so"
19
+
20
+ # ---- native library location ---------------------------------------
21
+ def self.native_candidates
22
+ lib_dir = File.expand_path("..", __dir__) # .../lib
23
+ here = __dir__ # .../lib/ccharts
24
+ repo_ext = File.expand_path("../../ext/ccharts", here) # repo build dir
25
+ [
26
+ File.join(lib_dir, "ccharts_ext.#{DLEXT}"), # dev build drops it in lib/
27
+ File.join(here, "ccharts_ext.#{DLEXT}"), # some install layouts
28
+ File.join(repo_ext, "ccharts_ext.#{DLEXT}"), # built in-place
29
+ ]
30
+ end
31
+
32
+ LIB = begin
33
+ path = native_candidates.find { |p| File.file?(p) }
34
+ raise LoadError, "ccharts native (ccharts_ext.#{DLEXT}) not built — run `rake compile` (or the Rakefile :compile task) first" unless path
35
+ Fiddle.dlopen(path)
36
+ end
37
+
38
+ # ---- type constants -------------------------------------------------
39
+ T_VOID = Fiddle::TYPE_VOID
40
+ T_VOIDP = Fiddle::TYPE_VOIDP
41
+ T_INT = Fiddle::TYPE_INT
42
+ T_DOUBLE = Fiddle::TYPE_DOUBLE
43
+ T_CHAR = Fiddle::TYPE_CHAR
44
+ T_LL = Fiddle::TYPE_LONG_LONG
45
+
46
+ def self.fn(name, args, ret)
47
+ Fiddle::Function.new(LIB[name], args, ret)
48
+ end
49
+
50
+ # ---- the 20 exported ccharts_* functions ----------------------------
51
+ FROM_ARRAYS = fn("ccharts_from_arrays", [T_VOIDP, T_VOIDP, T_VOIDP, T_VOIDP, T_VOIDP, T_INT, T_VOIDP], T_INT)
52
+ PARSE_JSON = fn("ccharts_parse_json", [T_VOIDP, T_VOIDP], T_INT)
53
+ PARSE_CSV = fn("ccharts_parse_csv", [T_VOIDP, T_CHAR, T_CHAR, T_VOIDP], T_INT)
54
+ DATA_LEN = fn("ccharts_data_len", [T_VOIDP], T_INT)
55
+ DATA_FREE = fn("ccharts_data_free", [T_VOIDP], T_VOID)
56
+ STRING_FREE = fn("ccharts_string_free", [T_VOIDP], T_VOID)
57
+ LINE = fn("ccharts_line", [T_VOIDP, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
58
+ CANDLE = fn("ccharts_candle", [T_VOIDP, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
59
+ PIE = fn("ccharts_pie_from_slices",
60
+ [T_VOIDP, T_INT, T_INT, T_INT, T_INT, T_VOIDP, T_INT, T_INT, T_INT,
61
+ T_DOUBLE, T_DOUBLE, T_INT, T_DOUBLE, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
62
+ HIST = fn("ccharts_hist", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
63
+ SPARK = fn("ccharts_spark", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
64
+ BAR = fn("ccharts_bar", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
65
+ STACK = fn("ccharts_stack", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
66
+ HEAT = fn("ccharts_heat", [T_VOIDP, T_INT, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
67
+ BOX = fn("ccharts_box", [T_VOIDP, T_INT, T_INT, T_INT, T_VOIDP, T_VOIDP, T_VOIDP], T_INT)
68
+ COLOR = fn("ccharts_color", [T_INT], T_VOIDP)
69
+ ERROR_MSG = fn("ccharts_error_message", [T_INT], T_VOIDP)
70
+ VERSION = fn("ccharts_version", [], T_VOIDP)
71
+ MAX_DIM = fn("ccharts_max_dim", [], T_INT)
72
+ MAX_CELLS = fn("ccharts_max_cells", [], T_INT)
73
+
74
+ # ---- settings structs (layouts from abi/ccharts_abi.h) --------------
75
+ # Every color is a pointer (void*); ints are int32. The Fiddle importer
76
+ # applies the same alignment/padding rules as the C compiler, so these
77
+ # match the ABI byte-for-byte.
78
+ Settings = struct [
79
+ "void* rise_color",
80
+ "void* fall_color",
81
+ "void* bg_color",
82
+ "void* area_color",
83
+ "int single_color",
84
+ "int show_prices",
85
+ "int show_times",
86
+ ]
87
+
88
+ PieSlice = struct [
89
+ "void* label",
90
+ "double value",
91
+ ]
92
+
93
+ HistSettings = struct [
94
+ "void* rise_color",
95
+ "void* bg_color",
96
+ "int bin_count",
97
+ "double min_value",
98
+ "double max_value",
99
+ "int show_bins",
100
+ "int show_prices",
101
+ ]
102
+
103
+ SparkSettings = struct [
104
+ "void* rise_color",
105
+ "void* area_color",
106
+ "int min_above",
107
+ "int min_below",
108
+ ]
109
+
110
+ BarSlice = struct [
111
+ "void* label",
112
+ "double value",
113
+ ]
114
+
115
+ BarSettings = struct [
116
+ "void* rise_color",
117
+ "void* bg_color",
118
+ "int show_labels",
119
+ "int show_prices",
120
+ ]
121
+
122
+ StackSeries = struct [
123
+ "void* name",
124
+ "void* values",
125
+ ]
126
+
127
+ StackSettings = struct [
128
+ "void* colors",
129
+ "void* bg_color",
130
+ "void* cat_labels",
131
+ "int series",
132
+ "int cats",
133
+ "int show_labels",
134
+ "int show_prices",
135
+ ]
136
+
137
+ HeatSettings = struct [
138
+ "void* low_color",
139
+ "void* high_color",
140
+ "void* mid_color",
141
+ "void* bg_color",
142
+ "void* row_labels",
143
+ "void* col_labels",
144
+ "int show_labels",
145
+ ]
146
+
147
+ BoxCategory = struct [
148
+ "void* name",
149
+ "void* samples",
150
+ "int n",
151
+ ]
152
+
153
+ BoxSettings = struct [
154
+ "void* rise_color",
155
+ "void* area_color",
156
+ "void* bg_color",
157
+ "int show_prices",
158
+ ]
159
+
160
+ # ---- helpers ---------------------------------------------------------
161
+ # A never-referenced empty string (plain mode: no ANSI escape at all).
162
+ EMPTY = Fiddle::Pointer["\0"]
163
+
164
+ # Turn a color value into a NUL-terminated escape pointer. `plain` forces
165
+ # the empty escape (overriding even a caller color) so no ANSI bytes are
166
+ # emitted. nil -> 0 (library default); else the resolved escape string.
167
+ def self.color_ptr(value, plain)
168
+ return EMPTY if plain
169
+ esc = Color.resolve(value)
170
+ return Fiddle::NULL if esc.nil?
171
+ Fiddle::Pointer[esc.b + "\0"]
172
+ end
173
+
174
+ # Pack a Ruby array of doubles into a C double[] pointer.
175
+ def self.pack_doubles(arr)
176
+ Fiddle::Pointer[arr.pack("d*")]
177
+ end
178
+
179
+ # Build ONE contiguous C array of `count` structs, each `size` bytes, so a
180
+ # `const ccharts_foo*`/array-of-structs argument points at a real C-layout
181
+ # block (`&arr[i]` is at `base + i*size`). Do NOT pass an array of pointers
182
+ # to separately-malloc'd structs — C reads this as a contiguous block and a
183
+ # pointer-array makes it read garbage at offset `i*size` (segfault / wrong
184
+ # data). `rows` is one entry per element: an array of `[offset, nbytes,
185
+ # pack_fmt, value]` field setters relative to that element's base.
186
+ def self.contiguous_structs(size, rows)
187
+ block = Fiddle::Pointer.malloc(size * rows.length)
188
+ rows.each_with_index do |fields, i|
189
+ base = i * size
190
+ fields.each { |off, nbytes, fmt, val| block[base + off, nbytes] = [val].pack(fmt) }
191
+ end
192
+ block
193
+ end
194
+
195
+ # Pack a Ruby array of integers into a C int64_t[] pointer.
196
+ def self.pack_i64(arr)
197
+ Fiddle::Pointer[arr.pack("q*")]
198
+ end
199
+
200
+ # NUL-terminated pointer for a Ruby string (or Fiddle::NULL for nil).
201
+ def self.cstr(str)
202
+ return Fiddle::NULL if str.nil?
203
+ Fiddle::Pointer[str.b + "\0"]
204
+ end
205
+
206
+ # Read a C string (NUL-terminated) from a returned pointer.
207
+ def self.read_cstr(ptr)
208
+ return nil if ptr.nil? || ptr.to_i == 0
209
+ ptr.to_s
210
+ end
211
+
212
+ # Read the integer value pointed to by a Fiddle::Pointer of the given size.
213
+ def self.read_ptr_int(buf, size)
214
+ buf[0, size].unpack1(size == 8 ? "Q" : "L")
215
+ end
216
+ end
217
+ end