You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
3.3 KiB

  1. #!/usr/bin/env ruby
  2. # All modules must be in the standard library. No third-party gems.
  3. require 'json'
  4. require 'date'
  5. require 'pp'
  6. def date_format(d)
  7. d.strftime("%-d %B %Y")
  8. end
  9. def divider
  10. puts '-' * 80
  11. end
  12. def first_word(s)
  13. s.split(" ").first
  14. end
  15. def subhead(s)
  16. puts
  17. puts
  18. puts s
  19. puts '-' * s.length
  20. end
  21. # FIXME
  22. # Need to be clear what the input and output of this function should be.
  23. # We should return an array of hashes (sorted?) not an array of arrays.
  24. # This is essentially a reduce function. Do it generically?
  25. # https://riptutorial.com/ruby/example/3624/inject--reduce
  26. # https://ruby-doc.org/core-3.1.2/Enumerable.html#method-i-reduce
  27. # https://stackoverflow.com/questions/4453511/group-hashes-by-keys-and-sum-the-values
  28. # Return party names and aggregate sum of votes for each party.
  29. def votes_by_party(ary_of_candidate_hashes)
  30. #pp ary_of_candidate_hashes
  31. output = {}
  32. ary_of_candidate_hashes.each do |candidate|
  33. if output.key?(candidate['party'])
  34. output[candidate['party']] += candidate['votes']
  35. else
  36. output[candidate['party']] = candidate['votes']
  37. end
  38. end
  39. #pp output
  40. output
  41. end
  42. # Load JSON data
  43. unless (param = ARGV.shift)
  44. abort "Usage: #{$0} [FILE] or #{$0} -- to read from STDIN."
  45. end
  46. if param == '--'
  47. # Read JSON data from STDIN
  48. json = ARGF.read
  49. else
  50. # Load JSON from file
  51. json = File.read(param)
  52. end
  53. data = JSON.parse(json)
  54. # Bodies
  55. data['bodies'].each do |body|
  56. puts body['name']
  57. puts "Elections: %d" % body['elections'].size
  58. # Elections
  59. sorted_elections = body['elections'].sort_by { |h| h['date'] }.reverse
  60. sorted_elections.each do |election|
  61. puts
  62. divider
  63. puts election['name'].upcase
  64. puts date_format(Date.parse(election['date']))
  65. puts election['reason'] if election['reason']
  66. puts "%d seats were elected in %d %s." % [ election['seats'],
  67. election['districts'], body['district_name_plural'] ]
  68. divider
  69. # Polls
  70. sorted_polls = election['polls'].sort_by { |h| h['district'] }
  71. sorted_polls.each do |poll|
  72. puts
  73. puts poll['district'].upcase
  74. puts
  75. # Candidates
  76. sorted_candidates = poll['candidates'].sort_by { |h| h['votes'] }.reverse
  77. sorted_candidates.each do |candidate|
  78. percentage = candidate['votes'].to_f / poll['votes'] * 100.0 *
  79. poll['seats']
  80. candidate['elected'] ? elected = "elected" : ""
  81. puts "%-31s %-30s %5d %2d%% %s" % [
  82. first_word(candidate['forenames']) + ' ' + candidate['surname'],
  83. candidate['party'], candidate['votes'], percentage, elected ]
  84. end
  85. # Total votes in this poll
  86. if poll['votes']
  87. puts
  88. puts "Total: %d" % poll['votes']
  89. end
  90. # Rejected ballots in this poll
  91. if poll['rejected']
  92. puts
  93. puts "Rejected ballots: %d" % poll['rejected']
  94. end
  95. # Votes by party
  96. # Omit for single-seat polls as meaningless there.
  97. unless poll["seats"] == 1
  98. subhead("Votes by party")
  99. vbp = votes_by_party(poll['candidates'])
  100. # Sort array of arrays by the value in element [1], descending
  101. sorted_vbp = vbp.sort { |a,b| b[1] <=> a[1] }
  102. #pp sorted_vbp
  103. sorted_vbp.each do |party|
  104. puts "%-30s %5d %2d%%" % [ party[0], party[1], party[1].to_f /
  105. poll["votes"] * 100.0 ]
  106. end
  107. end
  108. # Sources
  109. if poll["sources"]
  110. subhead("Sources")
  111. poll["sources"].each { |source| puts source }
  112. end
  113. puts
  114. divider
  115. end
  116. divider
  117. end
  118. end