main
  1#!/usr/bin/env ruby
  2
  3require 'bundler/inline'
  4
  5gemfile do
  6  source 'https://rubygems.org'
  7
  8  gem 'activesupport'
  9  gem 'actionview'
 10  gem 'csv'
 11end
 12
 13require 'action_view'
 14require 'action_view/helpers'
 15require 'active_support'
 16require 'active_support/core_ext/string'
 17require 'csv'
 18require 'erb'
 19
 20# https://data.calgary.ca/Demographics/Census-by-Community-2019/rkfr-buzb/about_data
 21# CLASS,CLASS_CODE,COMM_CODE,NAME,SECTOR,SRG,COMM_STRUCTURE,CNSS_YR,FOIP_IND,RES_CNT,DWELL_CNT,PRSCH_CHLD,ELECT_CNT,EMPLYD_CNT,OWNSHP_CNT,DOG_CNT,CAT_CNT,PUB_SCH,SEP_SCH,PUBSEP_SCH,OTHER_SCH,UNKNWN_SCH,SING_FAMLY,DUPLEX,MULTI_PLEX,APARTMENT,TOWN_HOUSE,MANUF_HOME,CONV_STRUC,COMUNL_HSE,RES_COMM,OTHER_RES,NURSING_HM,OTHER_INST,HOTEL_CNT,OTHER_MISC,APT_NO_RES,APT_OCCPD,APT_OWNED,APT_PERSON,APT_VACANT,APT_UC,APT_NA,CNV_NO_RES,CNV_OCCPD,CNV_OWNED,CNV_PERSON,CNV_VACANT,CNV_UC,CNV_NA,DUP_NO_RES,DUP_OCCPD,DUP_OWNED,DUP_PERSON,DUP_VACANT,DUP_UC,DUP_NA,MFH_NO_RES,MFH_OCCPD,MFH_OWNED,MFH_PERSON,MFH_VACANT,MFH_UC,MFH_NA,MUL_NO_RES,MUL_OCCPD,MUL_OWNED,MUL_PERSON,MUL_VACANT,MUL_UC,MUL_NA,OTH_NO_RES,OTH_OCCPD,OTH_OWNED,OTH_PERSON,OTH_VACANT,OTH_UC,OTH_NA,TWN_NO_RES,TWN_OCCPD,TWN_OWNED,TWN_PERSON,TWN_VACANT,TWN_UC,TWN_NA,SF_NO_RES,SF_OCCPD,SF_OWNED,SF_PERSON,SF_VACANT,SF_UC,SF_NA,OTH_STRTY,DWELSZ_1,DWELSZ_2,DWELSZ_3,DWELSZ_4_5,DWELSZ_6,MALE_CNT,FEMALE_CNT,MALE_0_4,MALE_5_14,MALE_15_19,MALE_20_24,MALE_25_34,MALE_35_44,MALE_45_54,MALE_55_64,MALE_65_74,MALE_75,FEM_0_4,FEM_5_14,FEM_15_19,FEM_20_24,FEM_25_34,FEM_35_44,FEM_45_54,FEM_55_64,FEM_65_74,FEM_75,MF_0_4,MF_5_14,MF_15_19,MF_20_24,MF_25_34,MF_35_44,MF_45_54,MF_55_64,MF_65_74,MF_75,OTHER_CNT,OTHER_0_4,OTHER_5_14,OTHER_15_19,OTHER_20_24,OTHER_25_34,OTHER_35_44,OTHER_45_54,OTHER_55_64,OTHER_65_74,OTHER_75,multipolygon
 22class Community
 23  include ActionView::Helpers::NumberHelper
 24
 25  def initialize(row)
 26    @row = row
 27  end
 28
 29  def name
 30    self[:name]
 31  end
 32
 33  def percent(key)
 34    return 0 if self[:res_cnt].to_i.zero?
 35
 36    ((self[key].to_f / self[:res_cnt].to_f) * 100).round(1)
 37  end
 38
 39  def homepage_url
 40    "https://www.calgary.ca/communities/profiles/#{name.parameterize}.html"
 41  end
 42
 43  def profile_url
 44    "https://www.calgary.ca/content/dam/www/csps/cns/documents/community_social_statistics/community-profiles/#{name.parameterize}.pdf"
 45  end
 46
 47  def projection_url
 48    "https://www.calgary.ca/content/dam/www/csps/cns/documents/community_social_statistics/#{name.parameterize}-pp.pdf"
 49  end
 50
 51  def [](key)
 52    @row.fetch(key.to_s.upcase)
 53  end
 54
 55  def <=>(other)
 56    [self[:sector], self.name] <=> [other[:sector], other.name]
 57  end
 58
 59  def properties
 60    Enumerator.new do |yielder|
 61      Property.each do |property|
 62        yielder.yield property if property[:comm_code] == self[:comm_code]
 63      end
 64    end
 65  end
 66
 67  def create_content_in(dir)
 68    target_dir = "#{dir}/#{self[:comm_code].parameterize}"
 69    FileUtils.mkdir_p(target_dir, verbose: true)
 70    create_readme_in(target_dir)
 71  end
 72
 73  private
 74
 75  def create_readme_in(dir)
 76    erb = ERB.new(IO.read("templates/community.md.erb"), trim_mode: "-")
 77    IO.write("#{dir}/README.md", erb.result(binding))
 78  end
 79end
 80
 81class Census
 82  include Enumerable
 83
 84  def initialize(path)
 85    @path = path
 86  end
 87
 88  def sectors
 89    map { |x| x[:sector] }.uniq
 90  end
 91
 92  def each
 93    CSV.foreach(@path, headers: true) do |row|
 94      community = Community.new(row)
 95      yield community if community[:class].include?("Residential")
 96    end
 97  end
 98
 99  def create_content_in(dir)
100    FileUtils.mkdir_p(dir, verbose: true)
101    erb = ERB.new(IO.read("templates/city.md.erb"), trim_mode: "-")
102    IO.write("#{dir}/README.md", erb.result(binding))
103
104    each do |community|
105      community.create_content_in(dir)
106    end
107  end
108end
109
110# https://data.calgary.ca/Government/Current-Year-Property-Assessments-Parcel-/4bsw-nn7w/about_data
111# ROLL_YEAR,ROLL_NUMBER,ADDRESS,ASSESSED_VALUE,ASSESSMENT_CLASS,ASSESSMENT_CLASS_DESCRIPTION,RE_ASSESSED_VALUE,NR_ASSESSED_VALUE,FL_ASSESSED_VALUE,COMM_CODE,COMM_NAME,YEAR_OF_CONSTRUCTION,LAND_USE_DESIGNATION,PROPERTY_TYPE,LAND_SIZE_SM,LAND_SIZE_SF,LAND_SIZE_AC,MOD_DATE,SUB_PROPERTY_USE,MULTIPOLYGON,UNIQUE_KEY
112class Property
113  def initialize(row)
114    @row = row
115  end
116
117  def [](key)
118    @row.fetch(key.to_s.upcase)
119  end
120
121  def residential?
122    self[:assessment_class_description] == "Residential"
123  end
124
125  def <=>(other)
126    self[:address] <=> other[:address]
127  end
128
129  class << self
130    def each(path = "data/Current_Year_Property_Assessments__Parcel__20240524.csv")
131      @properties ||= CSV
132        .read(path, headers: true)
133        .map { |x| Property.new(x) }
134        .sort
135      @properties.each do |property|
136        yield property if property.residential?
137      end
138    end
139  end
140end
141
142Census
143  .new("data/Census_by_Community_2019_20240524.csv")
144  .create_content_in("yyc")