omnizip 0.3.15 → 0.3.16
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.
- checksums.yaml +4 -4
- data/TODO.refactor/13-lutaml-model-migration.md +135 -71
- data/docs/getting-started/installation.adoc +1 -1
- data/lib/omnizip/algorithms/zstandard/encoder.rb +66 -38
- data/lib/omnizip/algorithms/zstandard/huffman_encoder.rb +49 -35
- data/lib/omnizip/algorithms/zstandard/match_finder.rb +343 -0
- data/lib/omnizip/algorithms/zstandard/sequences_encoder.rb +333 -0
- data/lib/omnizip/algorithms/zstandard.rb +3 -0
- data/lib/omnizip/converter.rb +17 -1
- data/lib/omnizip/models/conversion_options.rb +43 -42
- data/lib/omnizip/models/eta_result.rb +11 -12
- data/lib/omnizip/models/parallel_options.rb +59 -68
- data/lib/omnizip/models/progress_options.rb +22 -21
- data/lib/omnizip/parallel/parallel_compressor.rb +1 -1
- data/lib/omnizip/parallel/parallel_extractor.rb +1 -1
- data/lib/omnizip/parallel.rb +1 -1
- data/lib/omnizip/version.rb +1 -1
- data/readme-docs/advanced-features.adoc +1 -2
- data/readme-docs/format-converter.adoc +15 -24
- data/readme-docs/installation.adoc +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4417d9e90649e837f75b25bcee26c0721283e360d015eaf0c4194fa2957be053
|
|
4
|
+
data.tar.gz: 8c5fc8e793ea023d21abd9b044c3ddfb3786ee90225fe57fb322fa8cc60c5f4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2de569d977b1b47bc476efd9eb17e8f00c5b803e96451592964ee0f43bfa04caf52fd6462d02fbcaf9bd914b32436a210b1b935d24d6c1ad96a61acf119c7ca4
|
|
7
|
+
data.tar.gz: c070c98a67fef377cf8710f89ed922bb0a6ff5cf8edd0ac7307113f4973d07a85c5eb8dfce48210f7d570687d361f68cc61f947d4dc739588f9aee1662126269
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 13 — lutaml-model migration
|
|
2
2
|
|
|
3
3
|
Priority: **medium**.
|
|
4
|
-
Status:
|
|
5
|
-
|
|
4
|
+
Status: DONE for the models that should migrate. The remaining `to_h`
|
|
5
|
+
methods are deliberate — see "Why 17 stay" below.
|
|
6
6
|
|
|
7
7
|
## Rule
|
|
8
8
|
|
|
@@ -11,72 +11,136 @@ is follow-up).
|
|
|
11
11
|
> ... end` block. Never write `def to_h` / `def from_h` / `def to_json`
|
|
12
12
|
> / `def from_json` on a model class.
|
|
13
13
|
|
|
14
|
-
##
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
14
|
+
## Result
|
|
15
|
+
|
|
16
|
+
Six models now inherit `Lutaml::Model::Serializable`:
|
|
17
|
+
|
|
18
|
+
- `CompressionOptions`, `AlgorithmMetadata` (earlier pass)
|
|
19
|
+
- `ConversionOptions`, `ProgressOptions`, `ETAResult`, `ParallelOptions`
|
|
20
|
+
|
|
21
|
+
Counts, measured against the tree rather than estimated:
|
|
22
|
+
|
|
23
|
+
| Metric | Before | After |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `grep -rn "def to_h" lib/ \| wc -l` | 21 | 17 |
|
|
26
|
+
| Same under `lib/omnizip/models/` | 10 | 6 |
|
|
27
|
+
| `def to_json` / `from_h` / `from_json` in `lib/` | 0 | 0 |
|
|
28
|
+
|
|
29
|
+
An earlier version of this document claimed "26 `def to_h` across 22
|
|
30
|
+
files, 2 `def to_json`". Both were wrong; the numbers above are real.
|
|
31
|
+
|
|
32
|
+
## The symbol-type blocker was not real
|
|
33
|
+
|
|
34
|
+
This document previously held `ConversionOptions` back pending "lutaml
|
|
35
|
+
symbol-type support". There is nothing to wait for. `:symbol` is a
|
|
36
|
+
built-in type from 0.8.0 on: it is registered in `TYPE_CODES` in
|
|
37
|
+
`lib/lutaml/model/type.rb` and implemented in
|
|
38
|
+
`lib/lutaml/model/type/symbol.rb`. It casts symbols, strings, and nil
|
|
39
|
+
correctly and round-trips through JSON. `ConversionOptions` uses it for
|
|
40
|
+
`source_format`, `target_format`, `compression`, and `filter`.
|
|
41
|
+
|
|
42
|
+
The gemspec asks for `~> 0.8` because of this. `~> 0.7` also resolved to
|
|
43
|
+
0.8 on a fresh install, but it still admitted 0.7.1 through 0.7.7, where
|
|
44
|
+
`:symbol` is unregistered and `default: -> { detect_cpu_count }` is
|
|
45
|
+
evaluated against the class. Pin a build to 0.7.7 and 25 examples fail.
|
|
46
|
+
|
|
47
|
+
## Recipe
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "lutaml/model"
|
|
51
|
+
|
|
52
|
+
class Thing < Lutaml::Model::Serializable
|
|
53
|
+
attribute :level, :integer, default: 5
|
|
54
|
+
attribute :name, :string
|
|
55
|
+
|
|
56
|
+
key_value do
|
|
57
|
+
map "level", to: :level, render_default: true, render_nil: true
|
|
58
|
+
map "name", to: :name, render_default: true, render_nil: true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`key_value` covers hash, YAML, TOML, and JSON in one declaration.
|
|
64
|
+
|
|
65
|
+
### Two behaviours that will catch you out
|
|
66
|
+
|
|
67
|
+
**1. `to_hash` returns String keys.** The hand-rolled `to_h` methods
|
|
68
|
+
returned Symbol keys. These are not interchangeable, and
|
|
69
|
+
`Serializable` does not define `to_h` at all — calling it raises
|
|
70
|
+
`NoMethodError`. Removing `to_h` is a breaking change for any external
|
|
71
|
+
caller.
|
|
72
|
+
|
|
73
|
+
**2. `render_default: true` alone does not give you a fixed key set.**
|
|
74
|
+
It renders an unassigned default, but still drops a key that was
|
|
75
|
+
explicitly assigned `nil`:
|
|
76
|
+
|
|
77
|
+
| Mapping | fresh `.new` | after `x.a = nil` |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `render_default: true` | `{"a" => 5}` | `{}` |
|
|
80
|
+
| `+ render_nil: true` | `{"a" => 5, "b" => nil}` | `{"a" => nil}` |
|
|
81
|
+
|
|
82
|
+
Use **both** flags when the old `to_h` emitted every key. Use
|
|
83
|
+
`render_default` alone when it ended in `.compact`. Use neither when
|
|
84
|
+
there are no defaults and nils were dropped. There is no class-level or
|
|
85
|
+
global switch — `render_default:` is per-rule and defaults to `false`.
|
|
86
|
+
|
|
87
|
+
### Other gotchas
|
|
88
|
+
|
|
89
|
+
- Lambda defaults (`default: -> { detect_cpu_count }`) are evaluated
|
|
90
|
+
with `self` bound to the **instance**. A private instance method
|
|
91
|
+
works; a class method raises `NameError`.
|
|
92
|
+
- `dup` shares the internal `@using_default` hash. With both render
|
|
93
|
+
flags set this is unobservable, so no `dup` override or
|
|
94
|
+
`initialize_copy` hook is needed.
|
|
95
|
+
- Unknown keyword arguments to `.new` are silently ignored, where a
|
|
96
|
+
plain-Ruby `initialize` would raise `ArgumentError`.
|
|
97
|
+
- `self.class.attributes` returns a `Hash{Symbol=>Attribute}`. Iterate
|
|
98
|
+
it with `each_key`.
|
|
99
|
+
|
|
100
|
+
## Why 17 `to_h` methods stay
|
|
101
|
+
|
|
102
|
+
These are computed reports, projections over another object, or
|
|
103
|
+
wire-format builders — not serializations of the receiver's own state.
|
|
104
|
+
Converting them would drop the computed fields or force a second method
|
|
105
|
+
back onto the class. Leave them alone.
|
|
106
|
+
|
|
107
|
+
| File | Reason |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `models/conversion_result.rb` | Three computed fields; drops a stored one |
|
|
110
|
+
| `models/match_result.rb` | Keys don't match attributes; renames and computes |
|
|
111
|
+
| `models/optimization_suggestion.rb` | Computed `priority_score`; ctor validates enums |
|
|
112
|
+
| `models/performance_result.rb` | Computed fields plus `timestamp.iso8601` |
|
|
113
|
+
| `models/profile_report.rb` | Nested computed `summary:`; maps nested results |
|
|
114
|
+
| `models/filter_config.rb` | Emits `:name` for `@name_sym`; binary properties; registry lookups |
|
|
115
|
+
| `formats/rpm/header.rb` | Tag-map projection over a collection |
|
|
116
|
+
| `formats/xar/entry.rb` | XAR XML attributes; octal mode, formatted timestamps |
|
|
117
|
+
| `formats/zip/unix_extra_field.rb` | Emits a constant tag; wire format is manual pack |
|
|
118
|
+
| `metadata/entry_metadata.rb` | Pure delegation; stores no attributes of its own |
|
|
119
|
+
| `metadata/archive_metadata.rb` | Computed aggregates |
|
|
120
|
+
| `profile/compression_profile.rb` | `CustomProfile#to_h` calls `super` on it |
|
|
121
|
+
| `profile/custom_profile.rb` | Inheritance chain over `to_h` |
|
|
122
|
+
| `profile/profile_registry.rb` | Registry projection under a mutex |
|
|
123
|
+
| `progress/operation_progress.rb` | Computed percentages and elapsed time |
|
|
124
|
+
| `link_handler/symbolic_link.rb` | Type discriminator; storage uses `serialize` |
|
|
125
|
+
| `link_handler/hard_link.rb` | Type discriminator; storage uses `serialize` |
|
|
126
|
+
|
|
127
|
+
Because these stay, `grep "def to_h" lib/` will never reach zero. That
|
|
128
|
+
is the intended end state, not unfinished work.
|
|
129
|
+
|
|
130
|
+
## Follow-up work
|
|
131
|
+
|
|
132
|
+
1. **Rename the 17.** If they are reports and projections, `to_h` is
|
|
133
|
+
the wrong name — that is the real defect. Suggested groupings:
|
|
134
|
+
`to_report` for the computed ones, `to_tag_map` / `to_attributes`
|
|
135
|
+
for projections, `to_xml_attributes` / `to_fields` for wire-format
|
|
136
|
+
builders. Each rename is itself a public-API break.
|
|
137
|
+
2. **Fix `apply` in `CompressionOptions` and `AlgorithmMetadata`.**
|
|
138
|
+
Both iterate `self.class.attributes.each do |attr| ... attr.name`,
|
|
139
|
+
but `attributes` is a Hash, so the block gets a two-element Array
|
|
140
|
+
and `attr.name` raises `NoMethodError`. Neither has a caller or a
|
|
141
|
+
spec today, which is why it went unnoticed. Fix is `each_key`, plus
|
|
142
|
+
a spec per class.
|
|
143
|
+
3. **Extract the shared `apply` loop** once (2) has landed — three
|
|
144
|
+
classes now carry a similar body.
|
|
145
|
+
4. **Consider `FilterConfig` and the profile classes** for a proper
|
|
146
|
+
migration; both were judged too high-blast-radius to bundle here.
|
|
@@ -25,10 +25,13 @@ module Omnizip
|
|
|
25
25
|
class Zstandard
|
|
26
26
|
# Pure Ruby Zstandard encoder (RFC 8878).
|
|
27
27
|
#
|
|
28
|
-
# Emits a single-segment frame. Each 128 KiB chunk
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
28
|
+
# Emits a single-segment frame. Each 128 KiB chunk becomes one
|
|
29
|
+
# block, choosing whichever is smallest: RLE, a Compressed_Block
|
|
30
|
+
# (LZ77 match finding + Huffman literals + FSE-coded sequences),
|
|
31
|
+
# or a Raw_Block fallback for incompressible data. The match
|
|
32
|
+
# finder works over absolute positions, so blocks can reference
|
|
33
|
+
# earlier blocks; repeat-offset state is carried between blocks
|
|
34
|
+
# exactly as the decoder tracks it.
|
|
32
35
|
class Encoder
|
|
33
36
|
include Constants
|
|
34
37
|
|
|
@@ -83,53 +86,78 @@ module Omnizip
|
|
|
83
86
|
end
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
# rubocop:disable Metrics/MethodLength
|
|
90
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
86
91
|
def write_blocks(data)
|
|
87
92
|
return write_empty_last_block if data.empty?
|
|
88
93
|
|
|
94
|
+
params = MatchFinder.params_for_level(@level, data.bytesize)
|
|
95
|
+
ms = MatchFinder::MatchState.new(params[:hash_log])
|
|
96
|
+
ms.enable_chain(params[:chain]) if params[:chain].positive?
|
|
97
|
+
|
|
98
|
+
# Wire repeat-offset state carried across blocks; matches the
|
|
99
|
+
# decoder's executor state. Raw and RLE blocks leave it
|
|
100
|
+
# untouched.
|
|
101
|
+
reps = [1, 4, 8]
|
|
102
|
+
|
|
89
103
|
offset = 0
|
|
90
104
|
while offset < data.bytesize
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
block_end = [offset + BLOCK_MAX_SIZE, data.bytesize].min
|
|
106
|
+
is_last = block_end == data.bytesize
|
|
107
|
+
chunk = data.byteslice(offset, block_end - offset)
|
|
108
|
+
|
|
109
|
+
if chunk.bytesize >= 4 && rle_chunk?(chunk)
|
|
110
|
+
write_block_header(is_last ? 1 : 0, BLOCK_TYPE_RLE,
|
|
111
|
+
chunk.bytesize)
|
|
112
|
+
@output_stream.putc(chunk.getbyte(0))
|
|
113
|
+
else
|
|
114
|
+
seq_store = MatchFinder::SeqStore.new(reps.dup)
|
|
115
|
+
MatchFinder.compress_range(data, offset, block_end, seq_store,
|
|
116
|
+
ms, params[:min_match],
|
|
117
|
+
params[:lazy])
|
|
118
|
+
content, new_reps = try_compressed(seq_store, reps)
|
|
119
|
+
if content.nil?
|
|
120
|
+
write_block_header(is_last ? 1 : 0, BLOCK_TYPE_RAW,
|
|
121
|
+
chunk.bytesize)
|
|
122
|
+
@output_stream.write(chunk)
|
|
123
|
+
else
|
|
124
|
+
write_block_header(is_last ? 1 : 0, BLOCK_TYPE_COMPRESSED,
|
|
125
|
+
content.bytesize)
|
|
126
|
+
@output_stream.write(content)
|
|
127
|
+
reps = new_reps
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
offset = block_end
|
|
95
132
|
end
|
|
96
133
|
end
|
|
134
|
+
# rubocop:enable Metrics/MethodLength
|
|
97
135
|
|
|
98
136
|
def write_empty_last_block
|
|
99
137
|
write_block_header(1, BLOCK_TYPE_RAW, 0)
|
|
100
138
|
end
|
|
101
139
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Build a Compressed_Block content: literals section plus an
|
|
122
|
-
# empty sequences section (nbSeq = 0). Returns nil when the
|
|
123
|
-
# result is not smaller than the raw chunk.
|
|
124
|
-
def try_compressed(chunk)
|
|
125
|
-
literals_section = LiteralsEncoder.encode(chunk)
|
|
126
|
-
# A single 0x00 byte: Number_of_Sequences = 0.
|
|
127
|
-
content = literals_section + "\x00".b
|
|
128
|
-
return nil if content.bytesize >= chunk.bytesize
|
|
129
|
-
|
|
130
|
-
content
|
|
140
|
+
# Build a Compressed_Block content: literals section plus the
|
|
141
|
+
# sequences section. Returns [content, wire_reps], or [nil,
|
|
142
|
+
# nil] when the result is not smaller than the raw chunk.
|
|
143
|
+
#
|
|
144
|
+
# The chunk's decompressed size is the literal bytes plus every
|
|
145
|
+
# sequence's match length.
|
|
146
|
+
#
|
|
147
|
+
# @param initial_reps [Array<Integer>] wire rep state the
|
|
148
|
+
# decoder holds before this block
|
|
149
|
+
def try_compressed(seq_store, initial_reps)
|
|
150
|
+
literals_section = LiteralsEncoder.encode(seq_store.literals)
|
|
151
|
+
sequences_section, wire_reps =
|
|
152
|
+
SequencesEncoder.encode_section(seq_store, initial_reps)
|
|
153
|
+
content = literals_section + sequences_section
|
|
154
|
+
chunk_size = seq_store.literals.bytesize +
|
|
155
|
+
seq_store.sequences.sum(&:match_length)
|
|
156
|
+
return [nil, nil] if content.bytesize >= chunk_size
|
|
157
|
+
|
|
158
|
+
[content, wire_reps]
|
|
131
159
|
rescue Omnizip::CompressionError
|
|
132
|
-
nil
|
|
160
|
+
[nil, nil]
|
|
133
161
|
end
|
|
134
162
|
|
|
135
163
|
def rle_chunk?(chunk)
|
|
@@ -105,61 +105,75 @@ module Omnizip
|
|
|
105
105
|
lengths
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
# Cap code lengths at max_len
|
|
109
|
-
#
|
|
110
|
-
#
|
|
108
|
+
# Cap code lengths at max_len and repair the Kraft sum to be
|
|
109
|
+
# EXACTLY 2^max_len, in integer arithmetic. An exact sum is
|
|
110
|
+
# what lets the decoder re-derive the dropped last weight as a
|
|
111
|
+
# clean power of two — a float-tolerance repair can leave the
|
|
112
|
+
# remainder non-power-of-two and the frame undecodable.
|
|
111
113
|
#
|
|
112
114
|
# @param lengths [Array<Integer>] mutated in place
|
|
113
115
|
# @param max_len [Integer]
|
|
114
116
|
# @param freqs [Array<Integer>]
|
|
115
117
|
# @return [Array<Integer>]
|
|
118
|
+
# rubocop:disable Metrics/MethodLength
|
|
119
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
116
120
|
def limit_lengths(lengths, max_len, freqs)
|
|
117
|
-
|
|
118
|
-
cur_max = lengths.max || 0
|
|
119
|
-
break if cur_max <= max_len
|
|
121
|
+
lengths.map! { |l| [l, max_len].min }
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
next unless l == cur_max
|
|
124
|
-
next if longest && freqs[i] >= freqs[longest]
|
|
123
|
+
target = 1 << max_len
|
|
124
|
+
kraft = lengths.sum { |l| l.positive? ? 1 << (max_len - l) : 0 }
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
# Over-subscribed: lengthen the least-frequent shortest
|
|
127
|
+
# codes until the sum fits.
|
|
128
|
+
while kraft > target
|
|
129
|
+
idx = kraft_adjust_index(lengths, freqs) do |l|
|
|
130
|
+
l.positive? && l < max_len
|
|
127
131
|
end
|
|
132
|
+
lengths[idx] += 1
|
|
133
|
+
kraft -= 1 << (max_len - lengths[idx])
|
|
134
|
+
end
|
|
128
135
|
|
|
129
|
-
|
|
136
|
+
# Under-subscribed: shorten the most-frequent code that fits
|
|
137
|
+
# without overshooting; if none fits, lengthen one and let
|
|
138
|
+
# the first loop re-fit.
|
|
139
|
+
while kraft < target
|
|
140
|
+
best = nil
|
|
130
141
|
lengths.each_with_index do |l, i|
|
|
131
|
-
next unless l
|
|
132
|
-
next if shortest && freqs[i] <= freqs[shortest]
|
|
142
|
+
next unless l > 1
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
contribution = 1 << (max_len - l)
|
|
145
|
+
next if kraft + contribution > target
|
|
146
|
+
next if best && freqs[i] <= freqs[best]
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
best = i
|
|
149
|
+
end
|
|
150
|
+
if best.nil?
|
|
151
|
+
idx = kraft_adjust_index(lengths, freqs) do |l|
|
|
152
|
+
l.positive? && l < max_len
|
|
153
|
+
end
|
|
154
|
+
lengths[idx] += 1
|
|
155
|
+
kraft -= 1 << (max_len - lengths[idx])
|
|
140
156
|
else
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
delta = 1 << (max_len - lengths[best])
|
|
158
|
+
lengths[best] -= 1
|
|
159
|
+
kraft += delta
|
|
143
160
|
end
|
|
144
161
|
end
|
|
145
162
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
min_idx = nil
|
|
151
|
-
lengths.each_with_index do |l, i|
|
|
152
|
-
next unless l.positive? && l < max_len
|
|
153
|
-
next if min_idx && lengths[i] >= lengths[min_idx]
|
|
163
|
+
lengths
|
|
164
|
+
end
|
|
165
|
+
# rubocop:enable Metrics/MethodLength
|
|
154
166
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
# Index of the least-frequent symbol matching the block.
|
|
168
|
+
def kraft_adjust_index(lengths, freqs)
|
|
169
|
+
best = nil
|
|
170
|
+
lengths.each_with_index do |l, i|
|
|
171
|
+
next unless yield(l)
|
|
172
|
+
next if best && freqs[i] >= freqs[best]
|
|
158
173
|
|
|
159
|
-
|
|
174
|
+
best = i
|
|
160
175
|
end
|
|
161
|
-
|
|
162
|
-
lengths
|
|
176
|
+
best
|
|
163
177
|
end
|
|
164
178
|
|
|
165
179
|
# Serialize the weight table. Direct 4-bit weights when the
|