harvested 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. data/.gitignore +23 -0
  2. data/HISTORY +16 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +67 -0
  5. data/Rakefile +52 -0
  6. data/VERSION +1 -0
  7. data/examples/basics.rb +35 -0
  8. data/examples/clear_account.rb +28 -0
  9. data/examples/task_assignments.rb +27 -0
  10. data/examples/user_assignments.rb +24 -0
  11. data/features/account.feature +7 -0
  12. data/features/client_contacts.feature +23 -0
  13. data/features/clients.feature +29 -0
  14. data/features/errors.feature +25 -0
  15. data/features/expense_categories.feature +21 -0
  16. data/features/expenses.feature +55 -0
  17. data/features/hardy_client.feature +40 -0
  18. data/features/projects.feature +39 -0
  19. data/features/reporting.feature +72 -0
  20. data/features/step_definitions/account_steps.rb +7 -0
  21. data/features/step_definitions/assignment_steps.rb +100 -0
  22. data/features/step_definitions/contact_steps.rb +11 -0
  23. data/features/step_definitions/debug_steps.rb +3 -0
  24. data/features/step_definitions/error_steps.rb +113 -0
  25. data/features/step_definitions/expenses_steps.rb +46 -0
  26. data/features/step_definitions/harvest_steps.rb +8 -0
  27. data/features/step_definitions/model_steps.rb +90 -0
  28. data/features/step_definitions/people_steps.rb +4 -0
  29. data/features/step_definitions/report_steps.rb +91 -0
  30. data/features/step_definitions/time_entry_steps.rb +40 -0
  31. data/features/support/env.rb +37 -0
  32. data/features/support/error_helpers.rb +18 -0
  33. data/features/support/fixtures/empty_clients.xml +2 -0
  34. data/features/support/fixtures/over_limit.xml +8 -0
  35. data/features/support/fixtures/receipt.png +0 -0
  36. data/features/support/fixtures/under_limit.xml +8 -0
  37. data/features/support/harvest_credentials.example.yml +4 -0
  38. data/features/support/harvest_helpers.rb +11 -0
  39. data/features/support/inflections.rb +9 -0
  40. data/features/task_assignment.feature +69 -0
  41. data/features/tasks.feature +25 -0
  42. data/features/time_tracking.feature +29 -0
  43. data/features/user_assignments.feature +33 -0
  44. data/features/users.feature +55 -0
  45. data/lib/harvest/api/account.rb +10 -0
  46. data/lib/harvest/api/base.rb +42 -0
  47. data/lib/harvest/api/clients.rb +10 -0
  48. data/lib/harvest/api/contacts.rb +19 -0
  49. data/lib/harvest/api/expense_categories.rb +9 -0
  50. data/lib/harvest/api/expenses.rb +28 -0
  51. data/lib/harvest/api/projects.rb +39 -0
  52. data/lib/harvest/api/reports.rb +39 -0
  53. data/lib/harvest/api/task_assignments.rb +32 -0
  54. data/lib/harvest/api/tasks.rb +9 -0
  55. data/lib/harvest/api/time.rb +32 -0
  56. data/lib/harvest/api/user_assignments.rb +32 -0
  57. data/lib/harvest/api/users.rb +15 -0
  58. data/lib/harvest/base.rb +59 -0
  59. data/lib/harvest/base_model.rb +34 -0
  60. data/lib/harvest/behavior/activatable.rb +21 -0
  61. data/lib/harvest/behavior/crud.rb +31 -0
  62. data/lib/harvest/client.rb +18 -0
  63. data/lib/harvest/contact.rb +16 -0
  64. data/lib/harvest/credentials.rb +21 -0
  65. data/lib/harvest/errors.rb +23 -0
  66. data/lib/harvest/expense.rb +19 -0
  67. data/lib/harvest/expense_category.rb +18 -0
  68. data/lib/harvest/hardy_client.rb +80 -0
  69. data/lib/harvest/project.rb +22 -0
  70. data/lib/harvest/rate_limit_status.rb +16 -0
  71. data/lib/harvest/task.rb +20 -0
  72. data/lib/harvest/task_assignment.rb +34 -0
  73. data/lib/harvest/time_entry.rb +41 -0
  74. data/lib/harvest/timezones.rb +150 -0
  75. data/lib/harvest/user.rb +40 -0
  76. data/lib/harvest/user_assignment.rb +34 -0
  77. data/lib/harvested.rb +31 -0
  78. data/spec/harvest/base_spec.rb +9 -0
  79. data/spec/harvest/credentials_spec.rb +22 -0
  80. data/spec/harvest/expense_spec.rb +15 -0
  81. data/spec/harvest/task_assignment_spec.rb +10 -0
  82. data/spec/harvest/time_entry_spec.rb +22 -0
  83. data/spec/harvest/user_assignment_spec.rb +10 -0
  84. data/spec/harvest/user_spec.rb +32 -0
  85. data/spec/spec.default.opts +1 -0
  86. data/spec/spec_helper.rb +10 -0
  87. metadata +243 -0
@@ -0,0 +1,34 @@
1
+ module Harvest
2
+ class BaseModel
3
+ def initialize(attributes = {})
4
+ self.attributes = attributes
5
+ end
6
+
7
+ def attributes=(attributes)
8
+ attributes.each {|k,v| send("#{k}=", v)}
9
+ end
10
+
11
+ def ==(other)
12
+ id == other.id
13
+ end
14
+
15
+ def to_i
16
+ id
17
+ end
18
+
19
+ def to_xml
20
+ builder = Builder::XmlMarkup.new
21
+ builder.tag!(self.class.tag_name) do |c|
22
+ self.class.elements.each do |f|
23
+ c.tag!(f.tag, send(f.name)) if send(f.name)
24
+ end
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def api_path(path = nil)
30
+ @path ||= path
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ module Harvest
2
+ module Behavior
3
+ module Activatable
4
+ def deactivate(model)
5
+ if model.active?
6
+ request(:post, credentials, "#{api_model.api_path}/#{model.to_i}/toggle")
7
+ model.active = false
8
+ end
9
+ model
10
+ end
11
+
12
+ def activate(model)
13
+ if !model.active?
14
+ request(:post, credentials, "#{api_model.api_path}/#{model.to_i}/toggle")
15
+ model.active = true
16
+ end
17
+ model
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ module Harvest
2
+ module Behavior
3
+ module Crud
4
+ def all
5
+ response = request(:get, credentials, api_model.api_path)
6
+ api_model.parse(response.body)
7
+ end
8
+
9
+ def find(id)
10
+ response = request(:get, credentials, "#{api_model.api_path}/#{id}")
11
+ api_model.parse(response.body, :single => true)
12
+ end
13
+
14
+ def create(model)
15
+ response = request(:post, credentials, "#{api_model.api_path}", :body => model.to_xml)
16
+ id = response.headers["location"].first.match(/\/.*\/(\d+)/)[1]
17
+ find(id)
18
+ end
19
+
20
+ def update(model)
21
+ request(:put, credentials, "#{api_model.api_path}/#{model.to_i}", :body => model.to_xml)
22
+ find(model.id)
23
+ end
24
+
25
+ def delete(model)
26
+ request(:delete, credentials, "#{api_model.api_path}/#{model.to_i}")
27
+ model.id
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ module Harvest
2
+ class Client < BaseModel
3
+ include HappyMapper
4
+
5
+ api_path '/clients'
6
+
7
+ element :id, Integer
8
+ element :active, Boolean
9
+ element :name, String
10
+ element :details, String
11
+ element :currency, String
12
+ element :currency_symbol, String, :tag => "currency-symbol"
13
+ element :cache_version, Integer, :tag => "cache-version"
14
+ element :updated_at, Time, :tag => "updated-at"
15
+
16
+ alias_method :active?, :active
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module Harvest
2
+ class Contact < BaseModel
3
+ include HappyMapper
4
+
5
+ api_path '/contacts'
6
+
7
+ element :id, Integer
8
+ element :client_id, Integer, :tag => "client-id"
9
+ element :email, String
10
+ element :first_name, String, :tag => "first-name"
11
+ element :last_name, String, :tag => "last-name"
12
+ element :phone_office, String, :tag => "phone-office"
13
+ element :phone_mobile, String, :tag => "phone-mobile"
14
+ element :fax, String
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ module Harvest
2
+ class Credentials
3
+ attr_accessor :subdomain, :username, :password, :ssl
4
+
5
+ def initialize(subdomain, username, password, ssl = true)
6
+ @subdomain, @username, @password, @ssl = subdomain, username, password, ssl
7
+ end
8
+
9
+ def valid?
10
+ !subdomain.nil? && !username.nil? && !password.nil?
11
+ end
12
+
13
+ def basic_auth
14
+ Base64.encode64("#{username}:#{password}").delete("\r\n")
15
+ end
16
+
17
+ def host
18
+ "#{ssl ? "https" : "http"}://#{subdomain}.harvestapp.com"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Harvest
2
+ class InvalidCredentials < StandardError; end
3
+
4
+ class HTTPError < StandardError
5
+ attr_reader :response
6
+ def initialize(response)
7
+ @response = response
8
+ super
9
+ end
10
+
11
+ def to_s
12
+ hint = response.headers["hint"].nil? ? nil : response.headers["hint"].first
13
+ "#{self.class.to_s} : #{response.code}#{" - #{hint}" if hint}"
14
+ end
15
+ end
16
+
17
+ class RateLimited < HTTPError; end
18
+ class NotFound < HTTPError; end
19
+ class Unavailable < HTTPError; end
20
+ class InformHarvest < HTTPError; end
21
+ class BadRequest < HTTPError; end
22
+ class ServerError < HTTPError; end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Harvest
2
+ class Expense < BaseModel
3
+ include HappyMapper
4
+
5
+ api_path '/expenses'
6
+
7
+ element :id, Integer
8
+ element :notes, String
9
+ element :units, Integer
10
+ element :total_cost, Float, :tag => 'total-cost'
11
+ element :project_id, Integer, :tag => 'project-id'
12
+ element :expense_category_id, Integer, :tag => 'expense-category-id'
13
+ element :spent_at, Time, :tag => 'spent-at'
14
+
15
+ def spent_at=(date)
16
+ @spent_at = (String === date ? Time.parse(date) : date)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Harvest
2
+ class ExpenseCategory < BaseModel
3
+ include HappyMapper
4
+
5
+ tag 'expense-category'
6
+ api_path '/expense_categories'
7
+
8
+ element :id, Integer
9
+ element :name, String
10
+ element :unit_name, String, :tag => 'unit-name'
11
+ element :unit_price, Float, :tag => 'unit-price'
12
+ element :deactivated, Boolean
13
+
14
+ def active?
15
+ !deactivated
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,80 @@
1
+ module Harvest
2
+ class HardyClient < Delegator
3
+ def initialize(client, max_retries)
4
+ super(client)
5
+ @_sd_obj = @client = client
6
+ @max_retries = max_retries
7
+ (@client.public_methods - Object.public_instance_methods).each do |name|
8
+ instance_eval <<-END
9
+ def #{name}(*args)
10
+ wrap_collection do
11
+ @client.send('#{name}', *args)
12
+ end
13
+ end
14
+ END
15
+ end
16
+ end
17
+
18
+ def __getobj__; @_sd_obj; end
19
+ def __setobj__(obj); @_sd_obj = obj; end
20
+
21
+ def wrap_collection
22
+ collection = yield
23
+ HardyCollection.new(collection, self, @max_retries)
24
+ end
25
+
26
+ class HardyCollection < Delegator
27
+ def initialize(collection, client, max_retries)
28
+ super(collection)
29
+ @_sd_obj = @collection = collection
30
+ @client = client
31
+ @max_retries = max_retries
32
+ (@collection.public_methods - Object.public_instance_methods).each do |name|
33
+ instance_eval <<-END
34
+ def #{name}(*args)
35
+ retry_rate_limits do
36
+ @collection.send('#{name}', *args)
37
+ end
38
+ end
39
+ END
40
+ end
41
+ end
42
+
43
+ def __getobj__; @_sd_obj; end
44
+ def __setobj__(obj); @_sd_obj = obj; end
45
+
46
+ def retry_rate_limits
47
+ retries = 0
48
+
49
+ retry_func = lambda do |e|
50
+ if retries < @max_retries
51
+ retries += 1
52
+ true
53
+ else
54
+ raise e
55
+ end
56
+ end
57
+
58
+ begin
59
+ yield
60
+ rescue Harvest::RateLimited => e
61
+ seconds = if e.response.headers["retry-after"]
62
+ e.response.headers["retry-after"].first.to_i
63
+ else
64
+ 16
65
+ end
66
+ sleep(seconds)
67
+ retry
68
+ rescue Harvest::Unavailable, Harvest::InformHarvest => e
69
+ would_retry = retry_func.call(e)
70
+ sleep(16) if @client.account.rate_limit_status.over_limit?
71
+ retry if would_retry
72
+ rescue Net::HTTPError, Net::HTTPFatalError => e
73
+ retry if retry_func.call(e)
74
+ rescue SystemCallError => e
75
+ retry if e.is_a?(Errno::ECONNRESET) && retry_func.call(e)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ module Harvest
2
+ class Project < BaseModel
3
+ include HappyMapper
4
+
5
+ api_path '/projects'
6
+
7
+ element :id, Integer
8
+ element :client_id, Integer, :tag => 'client-id'
9
+ element :name, String
10
+ element :code, String
11
+ element :notes, String
12
+ element :fees, String
13
+ element :active, Boolean
14
+ element :billable, Boolean
15
+ element :budget, String
16
+ element :budget_by, Float, :tag => 'budget-by'
17
+ element :hourly_rate, Float, :tag => 'hourly-rate'
18
+ element :bill_by, String, :tag => 'bill-by'
19
+
20
+ alias_method :active?, :active
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module Harvest
2
+ class RateLimitStatus < BaseModel
3
+ include HappyMapper
4
+
5
+ tag 'hash'
6
+ element :last_access_at, Time, :tag => 'last-access-at'
7
+ element :count, Integer
8
+ element :timeframe_limit, Integer, :tag => 'timeframe-limit'
9
+ element :max_calls, Integer, :tag => 'max-calls'
10
+ element :lockout_seconds, Integer, :tag => 'lockout-seconds'
11
+
12
+ def over_limit?
13
+ count > max_calls
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ module Harvest
2
+ class Task < BaseModel
3
+ include HappyMapper
4
+
5
+ api_path '/tasks'
6
+
7
+ element :id, Integer
8
+ element :name, String
9
+ element :billable, Boolean, :tag => 'billable-by-default'
10
+ element :deactivated, Boolean, :tag => 'deactivated'
11
+ element :hourly_rate, Float, :tag => 'default-hourly-rate'
12
+ element :default, Boolean, :tag => 'is-default'
13
+
14
+ def active?
15
+ !deactivated
16
+ end
17
+
18
+ alias_method :default?, :default
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ module Harvest
2
+ class TaskAssignment < BaseModel
3
+ include HappyMapper
4
+
5
+ tag 'task-assignment'
6
+ element :id, Integer
7
+ element :task_id, Integer, :tag => 'task-id'
8
+ element :project_id, Integer, :tag => 'project-id'
9
+ element :billable, Boolean
10
+ element :deactivated, Boolean
11
+ element :hourly_rate, Float, :tag => 'hourly-rate'
12
+
13
+ def task=(task)
14
+ @task_id = task.to_i
15
+ end
16
+
17
+ def project=(project)
18
+ @project_id = project.to_i
19
+ end
20
+
21
+ def active?
22
+ !deactivated
23
+ end
24
+
25
+ def task_xml
26
+ builder = Builder::XmlMarkup.new
27
+ builder.task do |t|
28
+ t.id(task_id)
29
+ end
30
+ end
31
+
32
+ alias_method :billable?, :billable
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module Harvest
2
+ class TimeEntry < BaseModel
3
+ include HappyMapper
4
+
5
+ tag 'day_entry'
6
+
7
+ element :id, Integer
8
+ element :client, String
9
+ element :project, String
10
+ element :task, String
11
+ element :hours, Float
12
+ element :notes, String
13
+
14
+ element :project_id, Integer
15
+ element :task_id, Integer
16
+ element :spent_at, Time
17
+ element :created_at, Time
18
+ element :updated_at, Time
19
+ element :user_id, Integer
20
+ element :closed, Boolean, :tag => 'is-closed'
21
+ element :billed, Boolean, :tag => 'is-billed'
22
+
23
+ def spent_at=(date)
24
+ @spent_at = (String === date ? Time.parse(date) : date)
25
+ end
26
+
27
+ def to_xml
28
+ builder = Builder::XmlMarkup.new
29
+ builder.request do |r|
30
+ r.tag!('notes', notes) if notes
31
+ r.tag!('hours', hours) if hours
32
+ r.tag!('project_id', project_id) if project_id
33
+ r.tag!('task_id', task_id) if task_id
34
+ r.tag!('spent_at', spent_at) if spent_at
35
+ end
36
+ end
37
+
38
+ alias_method :closed?, :closed
39
+ alias_method :billed?, :billed
40
+ end
41
+ end
@@ -0,0 +1,150 @@
1
+ # shamelessly ripped from Rails: http://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb
2
+
3
+ module Harvest
4
+ module Timezones
5
+ MAPPING = {
6
+ "pacific/midway" => "International Date Line West",
7
+ "pacific/midway" => "Midway Island",
8
+ "pacific/pago_pago" => "Samoa",
9
+ "pacific/honolulu" => "Hawaii",
10
+ "america/juneau" => "Alaska",
11
+ "america/los_angeles" => "Pacific Time (US & Canada)",
12
+ "america/tijuana" => "Tijuana",
13
+ "america/denver" => "Mountain Time (US & Canada)",
14
+ "america/phoenix" => "Arizona",
15
+ "america/chihuahua" => "Chihuahua",
16
+ "america/mazatlan" => "Mazatlan",
17
+ "america/chicago" => "Central Time (US & Canada)",
18
+ "america/regina" => "Saskatchewan",
19
+ "america/mexico_city" => "Guadalajara",
20
+ "america/mexico_city" => "Mexico City",
21
+ "america/monterrey" => "Monterrey",
22
+ "america/guatemala" => "Central America",
23
+ "america/new_york" => "Eastern Time (US & Canada)",
24
+ "america/indiana/indianapolis" => "Indiana (East)",
25
+ "america/bogota" => "Bogota",
26
+ "america/lima" => "Lima",
27
+ "america/lima" => "Quito",
28
+ "america/halifax" => "Atlantic Time (Canada)",
29
+ "america/caracas" => "Caracas",
30
+ "america/la_paz" => "La Paz",
31
+ "america/santiago" => "Santiago",
32
+ "america/st_johns" => "Newfoundland",
33
+ "america/sao_paulo" => "Brasilia",
34
+ "america/argentina/buenos_aires" => "Buenos Aires",
35
+ "america/argentina/san_juan" => "Georgetown",
36
+ "america/godthab" => "Greenland",
37
+ "atlantic/south_georgia" => "Mid-Atlantic",
38
+ "atlantic/azores" => "Azores",
39
+ "atlantic/cape_verde" => "Cape Verde Is.",
40
+ "europe/dublin" => "Dublin",
41
+ "europe/dublin" => "Edinburgh",
42
+ "europe/lisbon" => "Lisbon",
43
+ "europe/london" => "London",
44
+ "africa/casablanca" => "Casablanca",
45
+ "africa/monrovia" => "Monrovia",
46
+ "etc/utc" => "UTC",
47
+ "europe/belgrade" => "Belgrade",
48
+ "europe/bratislava" => "Bratislava",
49
+ "europe/budapest" => "Budapest",
50
+ "europe/ljubljana" => "Ljubljana",
51
+ "europe/prague" => "Prague",
52
+ "europe/sarajevo" => "Sarajevo",
53
+ "europe/skopje" => "Skopje",
54
+ "europe/warsaw" => "Warsaw",
55
+ "europe/zagreb" => "Zagreb",
56
+ "europe/brussels" => "Brussels",
57
+ "europe/copenhagen" => "Copenhagen",
58
+ "europe/madrid" => "Madrid",
59
+ "europe/paris" => "Paris",
60
+ "europe/amsterdam" => "Amsterdam",
61
+ "europe/berlin" => "Berlin",
62
+ "europe/berlin" => "Bern",
63
+ "europe/rome" => "Rome",
64
+ "europe/stockholm" => "Stockholm",
65
+ "europe/vienna" => "Vienna",
66
+ "africa/algiers" => "West Central Africa",
67
+ "europe/bucharest" => "Bucharest",
68
+ "africa/cairo" => "Cairo",
69
+ "europe/helsinki" => "Helsinki",
70
+ "europe/kiev" => "Kyev",
71
+ "europe/riga" => "Riga",
72
+ "europe/sofia" => "Sofia",
73
+ "europe/tallinn" => "Tallinn",
74
+ "europe/vilnius" => "Vilnius",
75
+ "europe/athens" => "Athens",
76
+ "europe/istanbul" => "Istanbul",
77
+ "europe/minsk" => "Minsk",
78
+ "asia/jerusalem" => "Jerusalem",
79
+ "africa/harare" => "Harare",
80
+ "africa/johannesburg" => "Pretoria",
81
+ "europe/moscow" => "Moscow",
82
+ "europe/moscow" => "St. Petersburg",
83
+ "europe/moscow" => "Volgograd",
84
+ "asia/kuwait" => "Kuwait",
85
+ "asia/riyadh" => "Riyadh",
86
+ "africa/nairobi" => "Nairobi",
87
+ "asia/baghdad" => "Baghdad",
88
+ "asia/tehran" => "Tehran",
89
+ "asia/muscat" => "Abu Dhabi",
90
+ "asia/muscat" => "Muscat",
91
+ "asia/baku" => "Baku",
92
+ "asia/tbilisi" => "Tbilisi",
93
+ "asia/yerevan" => "Yerevan",
94
+ "asia/kabul" => "Kabul",
95
+ "asia/yekaterinburg" => "Ekaterinburg",
96
+ "asia/karachi" => "Islamabad",
97
+ "asia/karachi" => "Karachi",
98
+ "asia/tashkent" => "Tashkent",
99
+ "asia/kolkata" => "Chennai",
100
+ "asia/kolkata" => "Kolkata",
101
+ "asia/kolkata" => "Mumbai",
102
+ "asia/kolkata" => "New Delhi",
103
+ "asia/katmandu" => "Kathmandu",
104
+ "asia/dhaka" => "Astana",
105
+ "asia/dhaka" => "Dhaka",
106
+ "asia/colombo" => "Sri Jayawardenepura",
107
+ "asia/almaty" => "Almaty",
108
+ "asia/novosibirsk" => "Novosibirsk",
109
+ "asia/rangoon" => "Rangoon",
110
+ "asia/bangkok" => "Bangkok",
111
+ "asia/bangkok" => "Hanoi",
112
+ "asia/jakarta" => "Jakarta",
113
+ "asia/krasnoyarsk" => "Krasnoyarsk",
114
+ "asia/shanghai" => "Beijing",
115
+ "asia/chongqing" => "Chongqing",
116
+ "asia/hong_kong" => "Hong Kong",
117
+ "asia/urumqi" => "Urumqi",
118
+ "asia/kuala_lumpur" => "Kuala Lumpur",
119
+ "asia/singapore" => "Singapore",
120
+ "asia/taipei" => "Taipei",
121
+ "australia/perth" => "Perth",
122
+ "asia/irkutsk" => "Irkutsk",
123
+ "asia/ulaanbaatar" => "Ulaan Bataar",
124
+ "asia/seoul" => "Seoul",
125
+ "asia/tokyo" => "Osaka",
126
+ "asia/tokyo" => "Sapporo",
127
+ "asia/tokyo" => "Tokyo",
128
+ "asia/yakutsk" => "Yakutsk",
129
+ "australia/darwin" => "Darwin",
130
+ "australia/adelaide" => "Adelaide",
131
+ "australia/melbourne" => "Canberra",
132
+ "australia/melbourne" => "Melbourne",
133
+ "australia/sydney" => "Sydney",
134
+ "australia/brisbane" => "Brisbane",
135
+ "australia/hobart" => "Hobart",
136
+ "asia/vladivostok" => "Vladivostok",
137
+ "pacific/guam" => "Guam",
138
+ "pacific/port_moresby" => "Port Moresby",
139
+ "asia/magadan" => "Magadan",
140
+ "asia/magadan" => "Solomon Is.",
141
+ "pacific/noumea" => "New Caledonia",
142
+ "pacific/fiji" => "Fiji",
143
+ "asia/kamchatka" => "Kamchatka",
144
+ "pacific/majuro" => "Marshall Is.",
145
+ "pacific/auckland" => "Auckland",
146
+ "pacific/auckland" => "Wellington",
147
+ "pacific/tongatapu" => "Nuku'alofa"
148
+ }
149
+ end
150
+ end