encode_m 1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile +3 -0
- data/LICENSE +26 -0
- data/README.md +160 -0
- data/Rakefile +10 -0
- data/encode_m.gemspec +51 -0
- data/lib/encode_m/decoder.rb +39 -0
- data/lib/encode_m/encoder.rb +128 -0
- data/lib/encode_m/numeric.rb +133 -0
- data/lib/encode_m/version.rb +4 -0
- data/lib/encode_m.rb +31 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9d24f122adb312d76863b223d907c5f47a4e5ce5d88caf7de331606f62e54950
|
4
|
+
data.tar.gz: e0c8eb8bed9f7c6928aa6ba0c31c49420a0c189fc0adb5ba9f6db893451ee769
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b1a7532428f00fe47e62c3f8144f5e38331de0cb847260866f3bfd32c16718dfaaf4955fec2f472db0dd80f52bbc64be5ed8e06a8b0ff47d8c27c2fe49424ab2
|
7
|
+
data.tar.gz: 4220bda34fcbcc122a8539b38fecc4210f09e73e9482a2823c592bd60f6de189717926c1ba53d2a66b81dfd44da26e78d5752a90af9157d5c57f1a7c47f65929
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to the EncodeM project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [1.0.0] - 2025-09-03
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Initial release of EncodeM gem
|
12
|
+
- Core numeric encoding/decoding based on YottaDB/GT.M algorithm
|
13
|
+
- M() global convenience method for M language style
|
14
|
+
- Full arithmetic operations (+, -, *, /, **)
|
15
|
+
- Comparison operations maintaining sort order in encoded form
|
16
|
+
- Comprehensive test suite
|
17
|
+
- Documentation and examples
|
18
|
+
|
19
|
+
### Features
|
20
|
+
- Sortable byte encoding (key database feature)
|
21
|
+
- Optimized for common integer ranges
|
22
|
+
- 18-digit precision support
|
23
|
+
- Clean-room Ruby implementation of 40-year production algorithm
|
24
|
+
|
25
|
+
### Attribution
|
26
|
+
- Algorithm from YottaDB/GT.M (40 years in production)
|
27
|
+
- Ruby implementation by Steve Shreeve
|
28
|
+
- Implementation assistance from Claude Opus 4.1 (Anthropic)
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Steve Shreeve
|
4
|
+
|
5
|
+
Algorithm Credit: The numeric encoding algorithm implemented in this gem is
|
6
|
+
based on the work in YottaDB/GT.M, originally developed by Greystone Technology
|
7
|
+
Corporation in the 1980s and currently maintained by YottaDB LLC under AGPLv3.
|
8
|
+
This Ruby implementation is a clean-room reimplementation of the algorithm.
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# EncodeM
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/encode_m)
|
4
|
+
[](LICENSE)
|
5
|
+
|
6
|
+
Bringing the power of M language (MUMPS) numeric encoding to Ruby. Based on YottaDB/GT.M's 40-year production-tested algorithm.
|
7
|
+
|
8
|
+
## About the M Language Heritage
|
9
|
+
|
10
|
+
The M language (formerly MUMPS - Massachusetts General Hospital Utility Multi-Programming System) has been powering critical healthcare and financial systems since 1966. Epic (70% of US hospitals), the VA's VistA, and numerous banking systems run on M. This gem extracts one of M's most clever innovations: a numeric encoding that maintains sort order in byte form.
|
11
|
+
|
12
|
+
## Key Features
|
13
|
+
|
14
|
+
- **Sortable Byte Encoding**: Numbers encode to bytes that sort correctly without decoding
|
15
|
+
- **Production-Tested**: Algorithm proven in healthcare and finance for 40 years
|
16
|
+
- **Optimized for Real Use**: Special handling for common number ranges
|
17
|
+
- **Memory Efficient**: Compact representation, especially for small integers
|
18
|
+
- **Database-Friendly**: Perfect for indexing and byte-wise comparisons
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add to your Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'encode_m'
|
26
|
+
```
|
27
|
+
|
28
|
+
Or install directly:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
$ gem install encode_m
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'encode_m'
|
38
|
+
|
39
|
+
# Create numbers using the M() convenience method
|
40
|
+
a = M(42)
|
41
|
+
b = M(3.14)
|
42
|
+
c = M(-100)
|
43
|
+
|
44
|
+
# Arithmetic works naturally
|
45
|
+
sum = a + b # => EncodeM(45.14)
|
46
|
+
product = a * M(2) # => EncodeM(84)
|
47
|
+
|
48
|
+
# The magic: encoded bytes sort correctly!
|
49
|
+
numbers = [M(5), M(-10), M(0), M(100), M(-5)]
|
50
|
+
sorted = numbers.sort # Correctly sorted: -10, -5, 0, 5, 100
|
51
|
+
|
52
|
+
# Perfect for databases - compare without decoding
|
53
|
+
encoded_a = a.to_encoded # => "\x40\x42"
|
54
|
+
encoded_b = b.to_encoded # => "\x40\x03\x14"
|
55
|
+
encoded_a < encoded_b # => false (42 > 3.14)
|
56
|
+
|
57
|
+
# Decode back to numbers
|
58
|
+
original = EncodeM.decode(encoded_a) # => 42
|
59
|
+
```
|
60
|
+
|
61
|
+
## Why EncodeM?
|
62
|
+
|
63
|
+
Traditional numeric types force compromises:
|
64
|
+
|
65
|
+
| Type | Speed | Precision | Memory | Sortable as Bytes |
|
66
|
+
|------|-------|-----------|---------|-------------------|
|
67
|
+
| Integer | ⚡️ Fast | ✅ Exact | ✅ Efficient | ❌ No |
|
68
|
+
| Float | ⚡️ Fast | ❌ Limited | ✅ Efficient | ❌ No |
|
69
|
+
| BigDecimal | ❌ Slow | ✅ Unlimited | ❌ Heavy | ❌ No |
|
70
|
+
| **EncodeM** | ✅ Good | ✅ 18 digits | ✅ Efficient | ✅ **Yes!** |
|
71
|
+
|
72
|
+
EncodeM's unique advantage: encoded bytes maintain sort order, enabling:
|
73
|
+
- Direct byte comparison in databases
|
74
|
+
- Efficient indexing without decoding
|
75
|
+
- Fast range queries on encoded data
|
76
|
+
|
77
|
+
## Performance Characteristics
|
78
|
+
|
79
|
+
Based on the M language's real-world patterns:
|
80
|
+
- **Small integers (< 10)**: 2 bytes
|
81
|
+
- **Common range (-999 to 999)**: 2-3 bytes
|
82
|
+
- **Typical numbers (-10^9 to 10^9)**: 4-6 bytes
|
83
|
+
- **Sortable without decoding**: Massive performance win for databases
|
84
|
+
|
85
|
+
## Use Cases
|
86
|
+
|
87
|
+
- **Financial Systems**: More precision than Float, faster than BigDecimal
|
88
|
+
- **Database Indexing**: Sort encoded bytes directly
|
89
|
+
- **Healthcare Systems**: Proven in Epic, VistA, and other M-based systems
|
90
|
+
- **High-Volume Processing**: Efficient encoding for billions of records
|
91
|
+
- **Cross-System Integration**: Compatible with M language databases
|
92
|
+
|
93
|
+
## Attribution
|
94
|
+
|
95
|
+
This gem implements the numeric encoding algorithm from YottaDB and GT.M, which has been proven in production systems for nearly 40 years.
|
96
|
+
|
97
|
+
**Algorithm Credit**:
|
98
|
+
- Original design: Greystone Technology Corporation (1980s)
|
99
|
+
- Current implementations: [YottaDB](https://gitlab.com/YottaDB/DB/YDB) (AGPLv3) and GT.M
|
100
|
+
- Production proven in Epic, VistA, and Profile banking systems
|
101
|
+
|
102
|
+
**Ruby Implementation**:
|
103
|
+
- Author: Steve Shreeve (steve.shreeve@gmail.com)
|
104
|
+
- Implementation assistance: Claude Opus 4.1 (Anthropic)
|
105
|
+
- This is a clean-room reimplementation of the algorithm, not a code port
|
106
|
+
|
107
|
+
## Development
|
108
|
+
|
109
|
+
After checking out the repo, run:
|
110
|
+
|
111
|
+
```bash
|
112
|
+
bundle install
|
113
|
+
rake test
|
114
|
+
```
|
115
|
+
|
116
|
+
### Running Benchmarks
|
117
|
+
|
118
|
+
The gem includes two benchmark scripts in the `test/` directory:
|
119
|
+
|
120
|
+
```bash
|
121
|
+
# Performance benchmark - arithmetic and sorting operations
|
122
|
+
ruby -I lib test/benchmark.rb
|
123
|
+
|
124
|
+
# Database use case benchmark - demonstrates key benefits
|
125
|
+
ruby -I lib test/benchmark_database.rb
|
126
|
+
```
|
127
|
+
|
128
|
+
Note: You may need to install `bigdecimal` gem for Ruby 3.4+:
|
129
|
+
```bash
|
130
|
+
gem install bigdecimal
|
131
|
+
```
|
132
|
+
|
133
|
+
### Building and Installing
|
134
|
+
|
135
|
+
To install this gem locally:
|
136
|
+
|
137
|
+
```bash
|
138
|
+
bundle exec rake install
|
139
|
+
```
|
140
|
+
|
141
|
+
To release a new version:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
bundle exec rake release
|
145
|
+
```
|
146
|
+
|
147
|
+
## Contributing
|
148
|
+
|
149
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/shreeve/encode_m.
|
150
|
+
|
151
|
+
## License
|
152
|
+
|
153
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
154
|
+
|
155
|
+
## Acknowledgments
|
156
|
+
|
157
|
+
Special thanks to:
|
158
|
+
- The YottaDB team for maintaining and open-sourcing this technology
|
159
|
+
- The M language community for decades of innovation in database technology
|
160
|
+
- Anthropic's Claude Opus 4.1 for assistance with the Ruby implementation
|
data/Rakefile
ADDED
data/encode_m.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'lib/encode_m/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'encode_m'
|
5
|
+
spec.version = EncodeM::VERSION
|
6
|
+
spec.authors = ['Steve Shreeve']
|
7
|
+
spec.email = ['steve.shreeve@gmail.com']
|
8
|
+
|
9
|
+
spec.summary = 'M language numeric encoding for Ruby - sortable, efficient, production-tested'
|
10
|
+
spec.description = 'EncodeM brings a 40-year production-tested numeric encoding algorithm ' \
|
11
|
+
'from YottaDB/GT.M to Ruby. This algorithm from the M language (MUMPS) ' \
|
12
|
+
'provides efficient numeric handling with the unique property that ' \
|
13
|
+
'encoded byte strings maintain sort order. Perfect for database ' \
|
14
|
+
'operations, financial calculations, and systems requiring efficient ' \
|
15
|
+
'sortable number storage. A practical alternative between Float and ' \
|
16
|
+
'BigDecimal.'
|
17
|
+
spec.homepage = 'https://github.com/shreeve/encode_m'
|
18
|
+
spec.license = 'MIT'
|
19
|
+
spec.required_ruby_version = '>= 2.5.0'
|
20
|
+
|
21
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
22
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
23
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
24
|
+
spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
|
25
|
+
spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/encode_m"
|
26
|
+
|
27
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
28
|
+
`git ls-files -z`.split("\x0").reject { |f|
|
29
|
+
f.match(%r{^(test|spec|features)/}) ||
|
30
|
+
f.match(%r{^\.}) ||
|
31
|
+
f == 'Gemfile.lock'
|
32
|
+
}
|
33
|
+
end
|
34
|
+
spec.require_paths = ['lib']
|
35
|
+
|
36
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
37
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
38
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
39
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.6'
|
40
|
+
spec.add_development_dependency 'benchmark-ips', '~> 2.10'
|
41
|
+
|
42
|
+
spec.post_install_message = <<-MSG
|
43
|
+
Thank you for installing EncodeM!
|
44
|
+
|
45
|
+
Quick start:
|
46
|
+
require 'encode_m'
|
47
|
+
a = M(42) # Create a number with M language encoding
|
48
|
+
|
49
|
+
Learn more: https://github.com/shreeve/encode_m
|
50
|
+
MSG
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Decoder for M language numeric encoding
|
2
|
+
module EncodeM
|
3
|
+
class Decoder
|
4
|
+
POS_DECODE = Encoder::POS_CODE.each_with_index.map { |v, i| [v, i] }.to_h.freeze
|
5
|
+
NEG_DECODE = Encoder::NEG_CODE.each_with_index.map { |v, i| [v, i] }.to_h.freeze
|
6
|
+
|
7
|
+
def self.decode(encoded_bytes)
|
8
|
+
bytes = encoded_bytes.unpack('C*')
|
9
|
+
return 0 if bytes[0] == Encoder::SUBSCRIPT_ZERO
|
10
|
+
|
11
|
+
first_byte = bytes[0]
|
12
|
+
# Negatives are now < 0x40, positives are > 0x40, zero is 0x40
|
13
|
+
is_negative = first_byte < Encoder::SUBSCRIPT_ZERO
|
14
|
+
|
15
|
+
if is_negative
|
16
|
+
decode_table = NEG_DECODE
|
17
|
+
else
|
18
|
+
decode_table = POS_DECODE
|
19
|
+
end
|
20
|
+
|
21
|
+
mantissa = 0
|
22
|
+
|
23
|
+
bytes[1..-1].each do |byte|
|
24
|
+
break if byte == Encoder::NEG_MNTSSA_END || byte == Encoder::KEY_DELIMITER
|
25
|
+
|
26
|
+
digit_pair = decode_table[byte]
|
27
|
+
next unless digit_pair
|
28
|
+
|
29
|
+
mantissa = mantissa * 100 + digit_pair
|
30
|
+
end
|
31
|
+
|
32
|
+
# The mantissa contains the actual number value
|
33
|
+
# The exponent byte just determines sort order
|
34
|
+
result = mantissa
|
35
|
+
|
36
|
+
is_negative ? -result : result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# Encoding algorithm adapted from YottaDB/GT.M's mval2subsc.c
|
2
|
+
# This production-tested algorithm has powered M language systems since the 1980s
|
3
|
+
module EncodeM
|
4
|
+
class Encoder
|
5
|
+
# Constants from the M language subscript encoding
|
6
|
+
SUBSCRIPT_BIAS = 0x40
|
7
|
+
SUBSCRIPT_ZERO = 0x40
|
8
|
+
STR_SUB_PREFIX = 0x0A
|
9
|
+
STR_SUB_ESCAPE = 0x01
|
10
|
+
NEG_MNTSSA_END = 0xFF
|
11
|
+
KEY_DELIMITER = 0x00
|
12
|
+
SUBSCRIPT_STDCOL_NULL = 0xFF
|
13
|
+
|
14
|
+
# Encoding tables from YottaDB's production code
|
15
|
+
POS_CODE = [
|
16
|
+
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
|
17
|
+
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a,
|
18
|
+
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a,
|
19
|
+
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
|
20
|
+
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
|
21
|
+
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
|
22
|
+
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
|
23
|
+
0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
|
24
|
+
0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,
|
25
|
+
0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
NEG_CODE = [
|
29
|
+
0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5,
|
30
|
+
0xee, 0xed, 0xec, 0xeb, 0xea, 0xe9, 0xe8, 0xe7, 0xe6, 0xe5,
|
31
|
+
0xde, 0xdd, 0xdc, 0xdb, 0xda, 0xd9, 0xd8, 0xd7, 0xd6, 0xd5,
|
32
|
+
0xce, 0xcd, 0xcc, 0xcb, 0xca, 0xc9, 0xc8, 0xc7, 0xc6, 0xc5,
|
33
|
+
0xbe, 0xbd, 0xbc, 0xbb, 0xba, 0xb9, 0xb8, 0xb7, 0xb6, 0xb5,
|
34
|
+
0xae, 0xad, 0xac, 0xab, 0xaa, 0xa9, 0xa8, 0xa7, 0xa6, 0xa5,
|
35
|
+
0x9e, 0x9d, 0x9c, 0x9b, 0x9a, 0x99, 0x98, 0x97, 0x96, 0x95,
|
36
|
+
0x8e, 0x8d, 0x8c, 0x8b, 0x8a, 0x89, 0x88, 0x87, 0x86, 0x85,
|
37
|
+
0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78, 0x77, 0x76, 0x75,
|
38
|
+
0x6e, 0x6d, 0x6c, 0x6b, 0x6a, 0x69, 0x68, 0x67, 0x66, 0x65
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
def self.encode_integer(value)
|
42
|
+
return [SUBSCRIPT_ZERO].pack('C') if value == 0
|
43
|
+
|
44
|
+
is_negative = value < 0
|
45
|
+
mt = is_negative ? -value : value
|
46
|
+
cvt_table = is_negative ? NEG_CODE : POS_CODE
|
47
|
+
result = []
|
48
|
+
|
49
|
+
# Encode based on the number of digit pairs needed
|
50
|
+
# This maintains sort order and proper encoding/decoding
|
51
|
+
|
52
|
+
# Count digit pairs needed (each pair holds 00-99)
|
53
|
+
temp = mt
|
54
|
+
pairs = []
|
55
|
+
while temp > 0
|
56
|
+
pairs.unshift(temp % 100)
|
57
|
+
temp /= 100
|
58
|
+
end
|
59
|
+
|
60
|
+
# If no pairs (shouldn't happen for non-zero), add the number itself
|
61
|
+
pairs = [mt] if pairs.empty?
|
62
|
+
|
63
|
+
# The exponent represents the number of pairs
|
64
|
+
# For sorting: more pairs = larger magnitude
|
65
|
+
# We use SUBSCRIPT_BIAS + num_pairs to avoid conflict with SUBSCRIPT_ZERO
|
66
|
+
num_pairs = pairs.length
|
67
|
+
exp_byte = SUBSCRIPT_BIAS + num_pairs # Not -1, to stay above SUBSCRIPT_ZERO
|
68
|
+
|
69
|
+
# Encode the exponent byte
|
70
|
+
# For negatives, we need values < 0x40 that decrease as magnitude increases
|
71
|
+
# This ensures negatives sort before zero and in correct order
|
72
|
+
if is_negative
|
73
|
+
# Mirror the positive exponent below 0x40
|
74
|
+
# Larger magnitudes get smaller bytes for correct sorting
|
75
|
+
neg_exp_byte = 0x40 - (exp_byte - 0x40) - 1
|
76
|
+
result << neg_exp_byte
|
77
|
+
else
|
78
|
+
result << exp_byte
|
79
|
+
end
|
80
|
+
|
81
|
+
# Encode the mantissa pairs
|
82
|
+
pairs.each { |pair| result << cvt_table[pair] }
|
83
|
+
|
84
|
+
result << NEG_MNTSSA_END if is_negative && mt != 0
|
85
|
+
result.pack('C*')
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.encode_decimal(value, result = [])
|
89
|
+
str_val = value.to_s
|
90
|
+
is_negative = str_val.start_with?('-')
|
91
|
+
str_val = str_val[1..-1] if is_negative
|
92
|
+
|
93
|
+
parts = str_val.split('.')
|
94
|
+
integer_part = parts[0].to_i
|
95
|
+
|
96
|
+
exp = integer_part == 0 ? 0 : Math.log10(integer_part).floor + 1
|
97
|
+
mantissa = (str_val.delete('.').ljust(18, '0')[0...18]).to_i
|
98
|
+
|
99
|
+
cvt_table = is_negative ? NEG_CODE : POS_CODE
|
100
|
+
result << (is_negative ? ~(exp + SUBSCRIPT_BIAS) : (exp + SUBSCRIPT_BIAS))
|
101
|
+
|
102
|
+
temp = mantissa
|
103
|
+
digits = []
|
104
|
+
while temp > 0 && digits.length < 9
|
105
|
+
digits.unshift(temp % 100)
|
106
|
+
temp /= 100
|
107
|
+
end
|
108
|
+
|
109
|
+
digits.each { |pair| result << cvt_table[pair] }
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def self.encode_with_exp(mt, exp_val, is_negative, cvt_table, result)
|
116
|
+
result << (is_negative ? ~exp_val : exp_val)
|
117
|
+
|
118
|
+
pairs = []
|
119
|
+
temp = mt
|
120
|
+
while temp > 0
|
121
|
+
pairs.unshift(temp % 100)
|
122
|
+
temp /= 100
|
123
|
+
end
|
124
|
+
|
125
|
+
pairs.each { |pair| result << cvt_table[pair] }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# EncodeM::Numeric - Bringing M language efficiency to Ruby
|
2
|
+
module EncodeM
|
3
|
+
class Numeric
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
attr_reader :value, :encoded
|
7
|
+
|
8
|
+
# M language typically uses 18-digit precision
|
9
|
+
MAX_PRECISION = 18
|
10
|
+
|
11
|
+
def initialize(value)
|
12
|
+
@value = parse_value(value)
|
13
|
+
@encoded = Encoder.encode_integer(@value.is_a?(Integer) ? @value : @value.to_i)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_i
|
17
|
+
@value.to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_f
|
21
|
+
@value.to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
@value.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_encoded
|
29
|
+
@encoded
|
30
|
+
end
|
31
|
+
|
32
|
+
# Arithmetic operations
|
33
|
+
def +(other)
|
34
|
+
self.class.new(@value + coerce_value(other))
|
35
|
+
end
|
36
|
+
|
37
|
+
def -(other)
|
38
|
+
self.class.new(@value - coerce_value(other))
|
39
|
+
end
|
40
|
+
|
41
|
+
def *(other)
|
42
|
+
self.class.new(@value * coerce_value(other))
|
43
|
+
end
|
44
|
+
|
45
|
+
def /(other)
|
46
|
+
divisor = coerce_value(other)
|
47
|
+
raise ZeroDivisionError if divisor == 0
|
48
|
+
|
49
|
+
if @value.is_a?(Integer) && divisor.is_a?(Integer) && @value % divisor == 0
|
50
|
+
self.class.new(@value / divisor)
|
51
|
+
else
|
52
|
+
self.class.new(@value.to_f / divisor.to_f)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def **(other)
|
57
|
+
self.class.new(@value ** coerce_value(other))
|
58
|
+
end
|
59
|
+
|
60
|
+
# M language feature: encoded comparison
|
61
|
+
def <=>(other)
|
62
|
+
@encoded <=> self.class.new(other).encoded
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(other)
|
66
|
+
return false unless other.is_a?(self.class) || other.is_a?(::Numeric)
|
67
|
+
@value == coerce_value(other)
|
68
|
+
end
|
69
|
+
|
70
|
+
def abs
|
71
|
+
self.class.new(@value.abs)
|
72
|
+
end
|
73
|
+
|
74
|
+
def negative?
|
75
|
+
@value < 0
|
76
|
+
end
|
77
|
+
|
78
|
+
def positive?
|
79
|
+
@value > 0
|
80
|
+
end
|
81
|
+
|
82
|
+
def zero?
|
83
|
+
@value == 0
|
84
|
+
end
|
85
|
+
|
86
|
+
def round(n = 0)
|
87
|
+
if n == 0
|
88
|
+
self.class.new(@value.round)
|
89
|
+
else
|
90
|
+
self.class.new(@value.to_f.round(n))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Direct encoded comparison - key M language feature
|
95
|
+
def encoded_compare(other)
|
96
|
+
@encoded <=> other.encoded
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def parse_value(val)
|
102
|
+
case val
|
103
|
+
when Integer
|
104
|
+
val
|
105
|
+
when Float
|
106
|
+
raise ArgumentError, "Cannot represent Infinity" if val.infinite?
|
107
|
+
raise ArgumentError, "Cannot represent NaN" if val.nan?
|
108
|
+
val
|
109
|
+
when String
|
110
|
+
if val.include?('.')
|
111
|
+
Float(val)
|
112
|
+
else
|
113
|
+
Integer(val)
|
114
|
+
end
|
115
|
+
when self.class
|
116
|
+
val.value
|
117
|
+
else
|
118
|
+
raise ArgumentError, "Cannot convert #{val.class} to EncodeM::Numeric"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def coerce_value(other)
|
123
|
+
case other
|
124
|
+
when self.class
|
125
|
+
other.value
|
126
|
+
when ::Numeric
|
127
|
+
other
|
128
|
+
else
|
129
|
+
raise TypeError, "Cannot coerce #{other.class} with EncodeM::Numeric"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/encode_m.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# EncodeM - Bringing M language numeric encoding to Ruby
|
2
|
+
# Based on YottaDB/GT.M's 40-year production-tested algorithm
|
3
|
+
|
4
|
+
require 'encode_m/version'
|
5
|
+
require 'encode_m/encoder'
|
6
|
+
require 'encode_m/decoder'
|
7
|
+
require 'encode_m/numeric'
|
8
|
+
|
9
|
+
module EncodeM
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
# Factory method honoring M language convention
|
13
|
+
def self.new(value)
|
14
|
+
Numeric.new(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Decode - reverse the M encoding
|
18
|
+
def self.decode(encoded)
|
19
|
+
Decoder.decode(encoded)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Alias for M language users
|
23
|
+
def self.M(value)
|
24
|
+
Numeric.new(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Global convenience method (like M language global functions)
|
29
|
+
def M(value)
|
30
|
+
EncodeM::Numeric.new(value)
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: encode_m
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steve Shreeve
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: bundler
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.0'
|
19
|
+
type: :development
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rake
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '13.0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '13.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: minitest
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '5.0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '5.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: minitest-reporters
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.6'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.6'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: benchmark-ips
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '2.10'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '2.10'
|
82
|
+
description: EncodeM brings a 40-year production-tested numeric encoding algorithm
|
83
|
+
from YottaDB/GT.M to Ruby. This algorithm from the M language (MUMPS) provides efficient
|
84
|
+
numeric handling with the unique property that encoded byte strings maintain sort
|
85
|
+
order. Perfect for database operations, financial calculations, and systems requiring
|
86
|
+
efficient sortable number storage. A practical alternative between Float and BigDecimal.
|
87
|
+
email:
|
88
|
+
- steve.shreeve@gmail.com
|
89
|
+
executables: []
|
90
|
+
extensions: []
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- CHANGELOG.md
|
94
|
+
- Gemfile
|
95
|
+
- LICENSE
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- encode_m.gemspec
|
99
|
+
- lib/encode_m.rb
|
100
|
+
- lib/encode_m/decoder.rb
|
101
|
+
- lib/encode_m/encoder.rb
|
102
|
+
- lib/encode_m/numeric.rb
|
103
|
+
- lib/encode_m/version.rb
|
104
|
+
homepage: https://github.com/shreeve/encode_m
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata:
|
108
|
+
homepage_uri: https://github.com/shreeve/encode_m
|
109
|
+
source_code_uri: https://github.com/shreeve/encode_m
|
110
|
+
changelog_uri: https://github.com/shreeve/encode_m/blob/main/CHANGELOG.md
|
111
|
+
bug_tracker_uri: https://github.com/shreeve/encode_m/issues
|
112
|
+
documentation_uri: https://rubydoc.info/gems/encode_m
|
113
|
+
post_install_message: |
|
114
|
+
Thank you for installing EncodeM!
|
115
|
+
|
116
|
+
Quick start:
|
117
|
+
require 'encode_m'
|
118
|
+
a = M(42) # Create a number with M language encoding
|
119
|
+
|
120
|
+
Learn more: https://github.com/shreeve/encode_m
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: 2.5.0
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubygems_version: 3.7.1
|
136
|
+
specification_version: 4
|
137
|
+
summary: M language numeric encoding for Ruby - sortable, efficient, production-tested
|
138
|
+
test_files: []
|