///|
/// Exact top-strand base-count bin; multiplicity is never discarded.
pub(all) struct SizeBand {
length : Int
multiplicity : Int
} derive(Eq, Debug, ToJson)
///|
/// Group fragments by exact length, not gel mobility or molecular mass.
pub fn size_bands(
fragments : Array[Fragment],
) -> Array[SizeBand] raise RestrictError {
if fragments.is_empty() || fragments.length() > 10001 {
raise InvalidInput("BANDS_FRAGMENTS")
}
let sizes : Array[Int] = []
let mut total = 0
for f in fragments {
if f.length < 1 || f.length > 100000 || f.sequence.length() != f.length {
raise InvalidInput("BANDS_LENGTH")
}
total = total + f.length
if total > 100000 {
raise LimitExceeded("BANDS_TOTAL_BASES")
}
sizes.push(f.length)
}
sizes.sort()
let bands : Array[SizeBand] = []
for length in sizes {
let last = bands.length() - 1
if last >= 0 && bands[last].length == length {
bands[last] = { length, multiplicity: bands[last].multiplicity + 1 }
} else {
bands.push({ length, multiplicity: 1 })
}
}
bands
}
///|
/// A deterministic length-count string for comparison, independent of fragment order.
pub fn band_signature(
fragments : Array[Fragment],
) -> String raise RestrictError {
let out = StringBuilder::new()
for band in size_bands(fragments) {
out.write_string(
band.length.to_string() + "x" + band.multiplicity.to_string() + ";",
)
}
out.to_string()
}