blob: e27f13716eee03d551dba95d6fe8d7f466ecec5e [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:math' as math;
enum SortOrder {
/// From fewest occurrences to most.
ascending,
/// From most occurrences to fewest.
descending,
/// Lexicographically sorted by name.
alphabetical,
/// Keys are parsed as integers and sorted by value.
numeric,
}
/// Counts occurrences of strings and displays the results as a histogram.
class Histogram {
final Map<Object, int> _counts = {};
final SortOrder _order;
final bool _showBar;
final bool _showAll;
final int _minCount;
int get totalCount => _counts.values.fold(0, (a, b) => a + b);
Histogram({SortOrder? order, bool? showBar, bool? showAll, int? minCount})
: _order = order ?? SortOrder.descending,
_showBar = showBar ?? true,
_showAll = showAll ?? false,
_minCount = minCount ?? 0;
void add(Object item) {
_counts.putIfAbsent(item, () => 0);
_counts[item] = _counts[item]! + 1;
}
void printCounts(String label) {
var total = totalCount;
print('');
print('-- $label ($total total) --');
var keys = _counts.keys.toList();
switch (_order) {
case SortOrder.ascending:
keys.sort((a, b) => _counts[a]!.compareTo(_counts[b]!));
break;
case SortOrder.descending:
keys.sort((a, b) => _counts[b]!.compareTo(_counts[a]!));
break;
case SortOrder.alphabetical:
keys.sort();
break;
case SortOrder.numeric:
// TODO(rnystrom): Using string keys but treating them as integers is
// kind of hokey. But it keeps the [ScrapeVisitor] API simpler.
keys.sort((a, b) => (a as int).compareTo(b as int));
break;
}
var longest = keys.fold<int>(
0, (length, key) => math.max(length, key.toString().length));
var barScale = 80 - 22 - longest;
var shown = 0;
var skipped = 0;
for (var object in keys) {
var count = _counts[object]!;
var countString = count.toString().padLeft(7);
var percent = 100 * count / total;
var percentString = percent.toStringAsFixed(3).padLeft(7);
if (_showAll || ((shown < 100 || percent >= 0.1) && count >= _minCount)) {
var line = '$countString ($percentString%): $object';
if (_showBar && barScale > 1) {
line = line.padRight(longest + 22);
line += '=' * (percent / 100 * barScale).ceil();
}
print(line);
shown++;
} else {
skipped++;
}
}
if (skipped > 0) print('And $skipped more...');
// If we're counting numeric keys, show other statistics too.
if (_order == SortOrder.numeric && keys.isNotEmpty) {
var sum = keys.fold<int>(
0, (result, key) => result + (key as int) * _counts[key]!);
var average = sum / total;
// Find the median key where half the total count is below it.
var count = 0;
var median = -1;
for (var key in keys) {
count += _counts[key]!;
if (count >= total ~/ 2) {
median = key as int;
break;
}
}
print('Sum $sum, average ${average.toStringAsFixed(3)}, median $median');
}
}
}