Skip to content

Commit fc4dd11

Browse files
committed
Add methods for updating AUX tags and enhance tests for new functionalities
1 parent 0009f86 commit fc4dd11

4 files changed

Lines changed: 233 additions & 17 deletions

File tree

TUTORIAL.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,14 @@ in_bam.each do |record|
277277

278278
# Update or add tags using type-specific methods
279279
aux.update_int("AS", 100) # Integer tag
280+
aux.update_uint8("XI", 255) # Exact unsigned 8-bit integer tag
280281
aux.update_float("ZQ", 0.95) # Float tag
282+
aux.update_double("ZD", 0.125) # Double tag
283+
aux.update_char("XC", "Y") # Character tag
284+
aux.update_hex("XH", "DEADBEEF") # Hex string tag
281285
aux.update_string("RG", "sample1") # String tag
282-
aux.update_array("BC", [25, 30, 28, 32]) # Array tag
286+
aux.update_array("BC", [25, 30, 28, 32]) # Array tag (default subtype: i)
287+
aux.update_array("BQ", [25, 30, 28, 32], type: "C") # Array tag with explicit subtype
283288

284289
# Or use the []= operator (auto-detects type)
285290
aux["NM"] = 2 # Integer

lib/hts/bam/auxi.rb

Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,49 @@ def []=(key, value)
7979
# @param key [String] tag name (2 characters)
8080
# @param value [Integer] integer value
8181
def update_int(key, value)
82+
validate_tag!(key)
8283
ret = LibHTS.bam_aux_update_int(@record.struct, key, value.to_i)
8384
raise "Failed to update integer tag '#{key}': errno #{FFI.errno}" if ret < 0
8485

8586
value
8687
end
8788

89+
# Update or add a signed 8-bit integer tag.
90+
def update_int8(key, value)
91+
update_exact_integer(key, value, "c", -128, 127)
92+
end
93+
94+
# Update or add an unsigned 8-bit integer tag.
95+
def update_uint8(key, value)
96+
update_exact_integer(key, value, "C", 0, 255)
97+
end
98+
99+
# Update or add a signed 16-bit integer tag.
100+
def update_int16(key, value)
101+
update_exact_integer(key, value, "s", -32_768, 32_767)
102+
end
103+
104+
# Update or add an unsigned 16-bit integer tag.
105+
def update_uint16(key, value)
106+
update_exact_integer(key, value, "S", 0, 65_535)
107+
end
108+
109+
# Update or add a signed 32-bit integer tag.
110+
def update_int32(key, value)
111+
update_exact_integer(key, value, "i", -2_147_483_648, 2_147_483_647)
112+
end
113+
114+
# Update or add an unsigned 32-bit integer tag.
115+
def update_uint32(key, value)
116+
update_exact_integer(key, value, "I", 0, 4_294_967_295)
117+
end
118+
88119
# Update or add a floating-point tag
89120
# For compatibility with HTS.cr.
90121
# @param key [String] tag name (2 characters)
91122
# @param value [Float] floating-point value
92123
def update_float(key, value)
124+
validate_tag!(key)
93125
ret = LibHTS.bam_aux_update_float(@record.struct, key, value.to_f)
94126
raise "Failed to update float tag '#{key}': errno #{FFI.errno}" if ret < 0
95127

@@ -101,18 +133,51 @@ def update_float(key, value)
101133
# @param key [String] tag name (2 characters)
102134
# @param value [String] string value
103135
def update_string(key, value)
136+
validate_tag!(key)
104137
ret = LibHTS.bam_aux_update_str(@record.struct, key, -1, value.to_s)
105138
raise "Failed to update string tag '#{key}': errno #{FFI.errno}" if ret < 0
106139

107140
value
108141
end
109142

143+
# Update or add a character tag.
144+
def update_char(key, value)
145+
validate_tag!(key)
146+
147+
string = value.to_s
148+
raise ArgumentError, "Character AUX tags must be a single character" unless string.length == 1
149+
150+
replace_with_append(key, "A", string.b)
151+
string
152+
end
153+
154+
# Update or add a hexadecimal string tag.
155+
def update_hex(key, value)
156+
validate_tag!(key)
157+
158+
string = value.to_s
159+
raise ArgumentError, "Hex AUX tags must contain an even number of characters" if string.length.odd?
160+
raise ArgumentError, "Hex AUX tags must contain only hexadecimal characters" unless /\A[0-9A-Fa-f]*\z/.match?(string)
161+
162+
replace_with_append(key, "H", string.b + "\0")
163+
string
164+
end
165+
166+
# Update or add a double-precision floating-point tag.
167+
def update_double(key, value)
168+
validate_tag!(key)
169+
170+
replace_with_append(key, "d", [Float(value)].pack("E"))
171+
value.to_f
172+
end
173+
110174
# Update or add an array tag
111175
# For compatibility with HTS.cr.
112176
# @param key [String] tag name (2 characters)
113177
# @param value [Array] array of integers or floats
114178
# @param type [String, nil] element type ('c', 'C', 's', 'S', 'i', 'I', 'f'). Auto-detected if nil.
115179
def update_array(key, value, type: nil)
180+
validate_tag!(key)
116181
raise ArgumentError, "Array cannot be empty" if value.empty?
117182

118183
# Auto-detect type if not specified
@@ -127,21 +192,10 @@ def update_array(key, value, type: nil)
127192
end
128193
end
129194

130-
# Convert array to appropriate C type
131-
case type
132-
when "c", "C", "s", "S", "i", "I"
133-
# Integer types
134-
ptr = FFI::MemoryPointer.new(:int32, value.size)
135-
ptr.write_array_of_int32(value.map(&:to_i))
136-
ret = LibHTS.bam_aux_update_array(@record.struct, key, type.ord, value.size, ptr)
137-
when "f"
138-
# Float type
139-
ptr = FFI::MemoryPointer.new(:float, value.size)
140-
ptr.write_array_of_float(value.map(&:to_f))
141-
ret = LibHTS.bam_aux_update_array(@record.struct, key, type.ord, value.size, ptr)
142-
else
143-
raise ArgumentError, "Invalid array type: #{type}"
144-
end
195+
payload = pack_array_payload(value, type)
196+
ptr = FFI::MemoryPointer.new(:uint8, payload.bytesize)
197+
ptr.put_bytes(0, payload)
198+
ret = LibHTS.bam_aux_update_array(@record.struct, key, type.ord, value.size, ptr)
145199

146200
raise "Failed to update array tag '#{key}': errno #{FFI.errno}" if ret < 0
147201

@@ -213,6 +267,86 @@ def first_pointer
213267
LibHTS.bam_aux_first(@record.struct)
214268
end
215269

270+
def validate_tag!(key)
271+
raise ArgumentError, "AUX tag must be a 2-character String" unless key.is_a?(String) && key.length == 2
272+
end
273+
274+
def update_exact_integer(key, value, type, min, max)
275+
validate_tag!(key)
276+
277+
integer = Integer(value)
278+
raise RangeError, "Value #{integer} is out of range for AUX type #{type}" unless integer.between?(min, max)
279+
280+
replace_with_append(key, type, pack_scalar_payload(integer, type))
281+
integer
282+
end
283+
284+
def replace_with_append(key, type, payload)
285+
delete(key) if key?(key)
286+
287+
ptr = FFI::MemoryPointer.new(:uint8, payload.bytesize)
288+
ptr.put_bytes(0, payload)
289+
ret = LibHTS.bam_aux_append(@record.struct, key, type.ord, payload.bytesize, ptr)
290+
raise "Failed to update #{type} tag '#{key}': errno #{FFI.errno}" if ret < 0
291+
292+
true
293+
end
294+
295+
def pack_scalar_payload(value, type)
296+
case type
297+
when "c"
298+
[value].pack("c")
299+
when "C"
300+
[value].pack("C")
301+
when "s"
302+
[value].pack("s<")
303+
when "S"
304+
[value].pack("S<")
305+
when "i"
306+
[value].pack("l<")
307+
when "I"
308+
[value].pack("L<")
309+
else
310+
raise ArgumentError, "Unsupported scalar AUX type: #{type}"
311+
end
312+
end
313+
314+
def pack_array_payload(value, type)
315+
case type
316+
when "c"
317+
validate_integer_array_range!(value, -128, 127, type)
318+
value.pack("c*")
319+
when "C"
320+
validate_integer_array_range!(value, 0, 255, type)
321+
value.pack("C*")
322+
when "s"
323+
validate_integer_array_range!(value, -32_768, 32_767, type)
324+
value.pack("s<*")
325+
when "S"
326+
validate_integer_array_range!(value, 0, 65_535, type)
327+
value.pack("S<*")
328+
when "i"
329+
validate_integer_array_range!(value, -2_147_483_648, 2_147_483_647, type)
330+
value.pack("l<*")
331+
when "I"
332+
validate_integer_array_range!(value, 0, 4_294_967_295, type)
333+
value.pack("L<*")
334+
when "f"
335+
value.map(&:to_f).pack("e*")
336+
else
337+
raise ArgumentError, "Invalid array type: #{type}"
338+
end
339+
end
340+
341+
def validate_integer_array_range!(value, min, max, type)
342+
value.each do |element|
343+
integer = Integer(element)
344+
unless integer.between?(min, max)
345+
raise RangeError, "Array element #{integer} is out of range for AUX array type #{type}"
346+
end
347+
end
348+
end
349+
216350
def get_ruby_aux(aux_ptr, type = nil)
217351
type = type ? type.to_s : aux_ptr.read_string(1)
218352

lib/hts/libhts/sam.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def self.sam_itr_next(htsfp, itr, r)
508508
# Append tag data to a bam record
509509
attach_function \
510510
:bam_aux_append,
511-
[Bam1, :string, :string, :int, :pointer],
511+
[Bam1, :string, :char, :int, :pointer],
512512
:int
513513

514514
# Delete tag data from a bam record

test/bam_test.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,61 @@ def test_aux_update_string
405405
bam.close
406406
end
407407

408+
def test_aux_update_char
409+
bam = HTS::Bam.new(path_bam_string)
410+
record = bam.first
411+
aux = record.aux
412+
413+
aux.update_char("YC", "N")
414+
assert_equal "N", aux["YC"]
415+
416+
bam.close
417+
end
418+
419+
def test_aux_update_hex
420+
bam = HTS::Bam.new(path_bam_string)
421+
record = bam.first
422+
aux = record.aux
423+
424+
aux.update_hex("YH", "DEADBEEF")
425+
assert_equal "DEADBEEF", aux["YH"]
426+
427+
bam.close
428+
end
429+
430+
def test_aux_update_double
431+
bam = HTS::Bam.new(path_bam_string)
432+
record = bam.first
433+
aux = record.aux
434+
435+
aux.update_double("YD", 6.25)
436+
assert_in_delta 6.25, aux["YD"], 0.0001
437+
438+
bam.close
439+
end
440+
441+
def test_aux_update_typed_integers
442+
bam = HTS::Bam.new(path_bam_string)
443+
record = bam.first
444+
aux = record.aux
445+
446+
aux.update_int8("A1", -5)
447+
aux.update_uint8("A2", 250)
448+
aux.update_int16("A3", -1024)
449+
aux.update_uint16("A4", 60_000)
450+
aux.update_int32("A5", -1_000_000)
451+
aux.update_uint32("A6", 4_000_000_000)
452+
453+
assert_equal(-5, aux["A1"])
454+
assert_equal(250, aux["A2"])
455+
assert_equal(-1024, aux["A3"])
456+
assert_equal(60_000, aux["A4"])
457+
assert_equal(-1_000_000, aux["A5"])
458+
assert_equal(4_000_000_000, aux["A6"])
459+
460+
bam.close
461+
end
462+
408463
def test_aux_update_array_int
409464
bam = HTS::Bam.new(path_bam_string)
410465
record = bam.first
@@ -434,6 +489,28 @@ def test_aux_update_array_float
434489
bam.close
435490
end
436491

492+
def test_aux_update_array_with_uint8_subtype
493+
bam = HTS::Bam.new(path_bam_string)
494+
record = bam.first
495+
aux = record.aux
496+
497+
aux.update_array("ZC", [1, 2, 255], type: "C")
498+
assert_equal [1, 2, 255], aux["ZC"]
499+
500+
bam.close
501+
end
502+
503+
def test_aux_update_hex_validation
504+
bam = HTS::Bam.new(path_bam_string)
505+
record = bam.first
506+
aux = record.aux
507+
508+
assert_raises(ArgumentError) { aux.update_hex("YH", "ABC") }
509+
assert_raises(ArgumentError) { aux.update_hex("YH", "GG") }
510+
511+
bam.close
512+
end
513+
437514
def test_aux_bracket_assignment
438515
bam = HTS::Bam.new(path_bam_string)
439516
record = bam.first

0 commit comments

Comments
 (0)