| // 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); | |
| new({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'); | |
| } | |
| } | |
| } |