Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions python/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
- Add ``TreeSequence.map_to_vcf_model`` method to return a mapping of
the tree sequence to the VCF model.
(:user:`benjeffery`, :pr:`3163`)
- Use a thin space as the thousands separator in HTML output,
and a comma in CLI output.
(:user:`hossam26644`, :pr:`3167`, :issue:`2951`)

**Fixes**

Expand All @@ -38,6 +41,7 @@
to the required pattern.
(:user:`benjeffery`, :pr:`3163`)


--------------------
[0.6.3] - 2025-04-28
--------------------
Expand Down
14 changes: 7 additions & 7 deletions python/tests/test_highlevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3764,19 +3764,19 @@ def test_str(self, ts_fixture):
╔═+╗\s*
║Tree.*?║\s*
╠═+╤═+╣\s*
║Index.*?│\s*\d+║\s*
║Index.*?│\s*[\d\u2009,]+║\s*
╟─+┼─+╢\s*
║Interval.*?│\s*\d+-\d+\s*\(\d+\)║\s*
║Interval.*?│\s*[\d\u2009,]+-[\d\u2009,]+\s*\([\d\u2009,]+\)║\s*
╟─+┼─+╢\s*
║Roots.*?│\s*\d+║\s*
║Roots.*?│\s*[\d\u2009,]+║\s*
╟─+┼─+╢\s*
║Nodes.*?│\s*\d+║\s*
║Nodes.*?│\s*[\d\u2009,]+║\s*
╟─+┼─+╢\s*
║Sites.*?│\s*\d+║\s*
║Sites.*?│\s*[\d\u2009,]+║\s*
╟─+┼─+╢\s*
║Mutations.*?│\s*\d+║\s*
║Mutations.*?│\s*[\d\u2009,]+║\s*
╟─+┼─+╢\s*
║Total\s*Branch\s*Length.*?│\s*[\d,]+\.\d+║\s*
║Total\s*Branch\s*Length.*?│\s*[\d\u2009,]+\.\d+║\s*
╚═+╧═+╝\s*
""",
re.VERBOSE | re.DOTALL,
Expand Down
13 changes: 13 additions & 0 deletions python/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,19 @@ def test_naturalsize(value, expected):
assert util.naturalsize(-value) == expected


def test_format_number():
assert util.format_number(0) == "0"
assert util.format_number("1.23") == "1.23"
assert util.format_number(3216546.34) == "3 216 546.3"
assert util.format_number(3216546.34, 9) == "3 216 546.34"
assert util.format_number(-3456.23) == "-3 456.23"
assert util.format_number(-3456.23, sep=",") == "-3,456.23"

with pytest.raises(TypeError) as e_info:
util.format_number("bad")
assert str(e_info.value) == "The string cannot be converted to a number"


Comment thread
benjeffery marked this conversation as resolved.
@pytest.mark.parametrize(
"obj, expected",
[
Expand Down
17 changes: 10 additions & 7 deletions python/tskit/genotypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,22 +334,25 @@ def __str__(self) -> str:
Return a plain text summary of the contents of a variant.
"""
try:
site_id = self.site.id
site_position = self.site.position
site_id = util.format_number(self.site.id, sep=",")
site_position = util.format_number(self.site.position, sep=",")
counts = self.counts()
freqs = self.frequencies()
samples = util.format_number(len(self.samples), sep=",")
num_alleles = util.format_number(self.num_alleles, sep=",")
rows = (
[
["Site id", f"{site_id:,}"],
["Site position", f"{site_position:,}"],
["Number of samples", f"{len(self.samples):,}"],
["Number of alleles", f"{self.num_alleles:,}"],
["Site id", f"{site_id}"],
["Site position", f"{site_position}"],
["Number of samples", f"{samples}"],
["Number of alleles", f"{num_alleles}"],
]
+ [
[
f"Samples with allele "
f"""{'missing' if k is None else "'" + k + "'"}""",
f"{counts[k]:,} ({freqs[k] * 100:.2g}%)",
f"{util.format_number(counts[k], sep=',')} "
f"({util.format_number(freqs[k] * 100, 2, sep=',')}%)",
]
for k in self.alleles
]
Expand Down
35 changes: 16 additions & 19 deletions python/tskit/trees.py
Original file line number Diff line number Diff line change
Expand Up @@ -2822,17 +2822,21 @@ def __str__(self):
Return a plain text summary of a tree in a tree sequence
"""
tree_rows = [
["Index", f"{self.index:,}"],
["Index", f"{util.format_number(self.index, sep=',')}"],
[
"Interval",
f"{self.interval.left:,.8g}-{self.interval.right:,.8g}"
f"({self.span:,.8g})",
f"{util.format_number(self.interval.left, sep=',')}-"
f"{util.format_number(self.interval.right, sep=',')}"
f"({util.format_number(self.span, sep=',')})",
],
["Roots", f"{util.format_number(self.num_roots, sep=',')}"],
["Nodes", f"{util.format_number(len(self.preorder()), sep=',')}"],
["Sites", f"{util.format_number(self.num_sites, sep=',')}"],
["Mutations", f"{util.format_number(self.num_mutations, sep=',')}"],
[
"Total Branch Length",
f"{util.format_number(self.total_branch_length, sep=',')}",
],
["Roots", f"{self.num_roots:,}"],
["Nodes", f"{len(self.preorder()):,}"],
["Sites", f"{self.num_sites:,}"],
["Mutations", f"{self.num_mutations:,}"],
["Total Branch Length", f"{self.total_branch_length:,.8g}"],
]
return util.unicode_table(tree_rows, title="Tree")

Expand Down Expand Up @@ -4399,17 +4403,10 @@ def __str__(self):
Return a plain text summary of the contents of a tree sequence
"""
ts_rows = [
["Trees", str(self.num_trees)],
[
"Sequence Length",
str(
int(self.sequence_length)
if self.discrete_genome
else self.sequence_length
),
],
["Trees", util.format_number(self.num_trees, sep=",")],
["Sequence Length", util.format_number(self.sequence_length, sep=",")],
["Time Units", self.time_units],
["Sample Nodes", str(self.num_samples)],
["Sample Nodes", util.format_number(self.num_samples, sep=",")],
["Total Size", util.naturalsize(self.nbytes)],
]
header = ["Table", "Rows", "Size", "Has Metadata"]
Expand All @@ -4418,7 +4415,7 @@ def __str__(self):
table_rows.append(
[
name.capitalize(),
f"{table.num_rows:,}",
f"{util.format_number(table.num_rows, sep=',')}",
util.naturalsize(table.nbytes),
(
"Yes"
Expand Down
54 changes: 37 additions & 17 deletions python/tskit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,26 @@ def unicode_table(
return "".join(out)


def format_number(number, sig_digits=8, sep="\u2009"):
"""
Format a number with with a separator to indicate thousands
and up to `sig_digits` significant digits using 'g' format.

number: int, float, or a numeric string.
sig_digits: int, number of significant digits to display.
sep: str, the separator to use for thousands, default is a thin space.
Returns a string.
"""
if isinstance(number, str):
try:
number = float(number)
except ValueError:
raise TypeError("The string cannot be converted to a number")

fmt = f",.{sig_digits}g"
return format(number, fmt).replace(",", sep)


def html_table(rows, *, header):
headers = "".join(f"<th>{h}</th>" for h in header)
rows = (
Expand Down Expand Up @@ -519,7 +539,7 @@ def tree_sequence_html(ts):
f"""
<tr>
<td>{name.capitalize()}</td>
<td>{table.num_rows:,}</td>
<td>{format_number(table.num_rows)}</td>
<td>{naturalsize(table.nbytes)}</td>
<td style="text-align: center;">
{'✅' if hasattr(table, "metadata") and len(table.metadata) > 0
Expand Down Expand Up @@ -599,10 +619,10 @@ def tree_sequence_html(ts):
</tr>
</thead>
<tbody>
<tr><td>Trees</td><td>{ts.num_trees:,}</td></tr>
<tr><td>Sequence Length</td><td>{ts.sequence_length:,}</td></tr>
<tr><td>Trees</td><td>{format_number(ts.num_trees)}</td></tr>
<tr><td>Sequence Length</td><td>{format_number(ts.sequence_length)}</td></tr>
<tr><td>Time Units</td><td>{ts.time_units}</td></tr>
<tr><td>Sample Nodes</td><td>{ts.num_samples:,}</td></tr>
<tr><td>Sample Nodes</td><td>{format_number(ts.num_samples)}</td></tr>
<tr><td>Total Size</td><td>{naturalsize(ts.nbytes)}</td></tr>
<tr>
<td>Metadata</td><td style="text-align: left;">{md}</td>
Expand Down Expand Up @@ -671,13 +691,13 @@ def tree_html(tree):
</tr>
</thead>
<tbody>
<tr><td>Index</td><td>{tree.index:,}</td></tr>
<tr><td>Interval</td><td>{tree.interval.left:,.8g}-{tree.interval.right:,.8g} ({tree.span:,.8g})</td></tr>
<tr><td>Roots</td><td>{tree.num_roots:,}</td></tr>
<tr><td>Nodes</td><td>{len(tree.preorder()):,}</td></tr>
<tr><td>Sites</td><td>{tree.num_sites:,}</td></tr>
<tr><td>Mutations</td><td>{tree.num_mutations:,}</td></tr>
<tr><td>Total Branch Length</td><td>{tree.total_branch_length:,.8g}</td></tr>
<tr><td>Index</td><td>{format_number(tree.index)}</td></tr>
<tr><td>Interval</td><td>{format_number(tree.interval.left)}-{format_number(tree.interval.right)} ({format_number(tree.span)})</td></tr>
<tr><td>Roots</td><td>{format_number(tree.num_roots)}</td></tr>
<tr><td>Nodes</td><td>{format_number(len(tree.preorder()))}</td></tr>
<tr><td>Sites</td><td>{format_number(tree.num_sites)}</td></tr>
<tr><td>Mutations</td><td>{format_number(tree.num_mutations)}</td></tr>
<tr><td>Total Branch Length</td><td>{format_number(tree.total_branch_length)}</td></tr>
</tbody>
</table>
</div>
Expand Down Expand Up @@ -746,18 +766,18 @@ def variant_html(variant):
return (
html_body_head
+ f"""
<tr><td>Site Id</td><td>{site_id:,}</td></tr>
<tr><td>Site Position</td><td>{site_position:,.8g}</td></tr>
<tr><td>Number of Samples</td><td>{num_samples:,}</td></tr>
<tr><td>Number of Alleles</td><td>{num_alleles:,}</td></tr>
<tr><td>Site Id</td><td>{format_number(site_id)}</td></tr>
<tr><td>Site Position</td><td>{format_number(site_position)}</td></tr>
<tr><td>Number of Samples</td><td>{format_number(num_samples)}</td></tr>
<tr><td>Number of Alleles</td><td>{format_number(num_alleles)}</td></tr>
"""
+ "\n".join(
[
f"""<tr><td>Samples with Allele {'missing' if k is None
else "'" + k + "'"}</td><td>"""
+ f"{counts[k]:,}"
+ f"{format_number(counts[k])}"
+ " "
+ f"({freqs[k] * 100:,.2g}%)"
+ f"({format_number(freqs[k] * 100, 2)}%)"
+ "</td></tr>"
for k in variant.alleles
]
Expand Down
Loading