Skip to content

Commit

Permalink
Add location aliases (#269)
Browse files Browse the repository at this point in the history
Resolves #242
  • Loading branch information
Jonas Fabisiak authored Oct 29, 2020
1 parent 71fcefe commit 4510bc8
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 22 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ lots of help), and give feedback! This project is
- [`add tag`](#add-tag)
- [`add location`](#add-location)
- [`add nickname`](#add-nickname)
- [`add alias`](#add-alias)
- [Adding a default location](#adding-a-default-location)
- [`clean`](#clean)
- [`graph`](#graph)
Expand All @@ -54,6 +55,7 @@ lots of help), and give feedback! This project is
- `remove`
- [`remove tag`](#remove-tag)
- [`remove nickname`](#remove-nickname)
- [`remove alias`](#remove-alias)
- `rename`
- [`rename friend`](#rename-friend)
- [`rename location`](#rename-location)
Expand Down Expand Up @@ -291,11 +293,13 @@ $ friends add activity Got lunch with Earnest H and Earnest S. in the park. Man,
Activity added: "2017-05-01: Got lunch with Earnest Hemingway and Earnest Shackleton in the park. Man, I like Earnest Hemingway but really love Earnest Shackleton."
```

And locations will be matched as well:
And locations or their aliases will be matched as well:

```bash
$ friends add activity Went swimming near atlantis with George.
Activity added: "2017-01-06: Went swimming near Atlantis with George Washington Carver."
$ friends add activity Had lunch in nyc with George.
Activity added: "2017-01-06: Had lunch in New York City with George Washington Carver."
```

Tags will be colored if they're provided (though this README can't display
Expand Down Expand Up @@ -411,6 +415,15 @@ $ friends add nickname "Grace Hopper" "Amazing Grace"
Nickname added: "Grace Hopper (a.k.a. The Admiral a.k.a. Amazing Grace)"
```
#### `add alias`
```bash
$ friends add alias "New York City" "NYC"
Alias added: "New York City (a.k.a. NYC)
$ friends add alias "New York City" "Big Apple"
Alias added: "New York City (a.k.a. NYC a.k.a. Big Apple)"
```

#### Setting a default location

When an activity includes the phrase to \_LOCATION\_ (e.g., Took a plane to \_Paris\_), all future activities that have no explicit location will be associated with that location:
Expand Down Expand Up @@ -876,6 +889,15 @@ $ friends remove nickname "Grace Hopper" "The Admiral"
Nickname removed: "Grace Hopper (a.k.a. Amazing Grace)"
```

#### `remove alias`

Removes a specific alias from a location:

```bash
$ friends remove alias "New York City" "Big Apple"
Alias removed: "New York City (a.k.a. NYC)"
```

#### `rename friend`

```bash
Expand Down
11 changes: 10 additions & 1 deletion lib/friends/commands/add.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

desc "Adds a friend (or nickname), activity, note, or location"
desc "Adds a friend (or nickname), activity, note, or location (or alias)"
command :add do |add|
add.desc "Adds a friend"
add.arg_name "NAME"
Expand Down Expand Up @@ -40,6 +40,15 @@
end
end

add.desc "Adds an alias to a location"
add.arg_name "LOCATION ALIAS"
add.command :alias do |add_alias|
add_alias.action do |_, _, args|
@introvert.add_alias(name: args.first.to_s.strip, nickname: args[1].to_s.strip)
@dirty = true # Mark the file for cleaning.
end
end

add.desc "Adds a tag to a friend"
add.arg_name "NAME @TAG"
add.command :tag do |add_tag|
Expand Down
7 changes: 5 additions & 2 deletions lib/friends/commands/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@

list.desc "List all locations"
list.command :locations do |list_locations|
list_locations.action do
@introvert.list_locations
list_locations.switch [:verbose],
negatable: false,
desc: "Output location aliases"
list_locations.action do |_, options|
@introvert.list_locations(verbose: options[:verbose])
end
end

Expand Down
9 changes: 9 additions & 0 deletions lib/friends/commands/remove.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
end
end

remove.desc "Removes an alias from a location"
remove.arg_name "LOCATION ALIAS"
remove.command :alias do |remove_alias|
remove_alias.action do |_, _, args|
@introvert.remove_alias(name: args.first.to_s.strip, nickname: args[1].to_s.strip)
@dirty = true # Mark the file for cleaning.
end
end

remove.desc "Removes a tag from a friend"
remove.arg_name "NAME @TAG"
remove.command :tag do |remove_tag|
Expand Down
56 changes: 46 additions & 10 deletions lib/friends/introvert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,31 @@ def add_nickname(name:, nickname:)
@output << "Nickname added: \"#{friend}\""
end

# Add an alias to an existing location.
# @param name [String] the name of the location
# @param nickname [String] the alias to add to the location
# @raise [FriendsError] if 0 or 2+ locations match the given name
# @raise [FriendsError] if the alias is already taken
def add_alias(name:, nickname:)
raise FriendsError, "Expected \"[Location Name]\" \"[Alias]\"" if name.empty?
raise FriendsError, "Alias cannot be blank" if nickname.empty?

collision = @locations.find do |loc|
loc.name.casecmp(nickname).zero? || loc.aliases.any? { |a| a.casecmp(nickname).zero? }
end

if collision
raise FriendsError,
"The location alias \"#{nickname}\" is already taken by "\
"\"#{collision}\""
end

location = thing_with_name_in(:location, name)
location.add_alias(nickname)

@output << "Alias added: \"#{location}\""
end

# Add a tag to an existing friend.
# @param name [String] the name of the friend
# @param tag [String] the tag to add to the friend, of the form: "@tag"
Expand Down Expand Up @@ -250,6 +275,21 @@ def remove_nickname(name:, nickname:)
@output << "Nickname removed: \"#{friend}\""
end

# Remove an alias from an existing location.
# @param name [String] the name of the location
# @param nickname [String] the alias to remove from the location
# @raise [FriendsError] if 0 or 2+ locations match the given name
# @raise [FriendsError] if the location does not have the given alias
def remove_alias(name:, nickname:)
raise FriendsError, "Expected \"[Location Name]\" \"[Alias]\"" if name.empty?
raise FriendsError, "Alias cannot be blank" if nickname.empty?

location = thing_with_name_in(:location, name)
location.remove_alias(nickname)

@output << "Alias removed: \"#{location}\""
end

# List all friend names in the friends file.
# @param location_name [String] the name of a location to filter by, or nil
# for unfiltered
Expand Down Expand Up @@ -297,8 +337,8 @@ def list_notes(**args)
end

# List all location names in the friends file.
def list_locations
@locations.each { |location| @output << location.name }
def list_locations(verbose:)
(verbose ? @locations.map(&:to_s) : @locations.map(&:name)).each { |line| @output << line }
end

# @param from [Array] containing any of: ["activities", "friends", "notes"]
Expand Down Expand Up @@ -429,16 +469,16 @@ def regex_friend_map
#
# The returned hash uses the following format:
# {
# /regex/ => [list of friends matching regex]
# /regex/ => location
# }
#
# This hash is sorted (because Ruby's hashes are ordered) by decreasing
# regex key length, so the key /Paris, France/ appears before /Paris/.
#
# @return [Hash{Regexp => Array<Friends::Location>}]
# @return [Hash{Regexp => location}]
def regex_location_map
@locations.each_with_object({}) do |location, hash|
hash[location.regex_for_name] = location
location.regexes_for_name.each { |regex| hash[regex] = location }
end.sort_by { |k, _| -k.to_s.size }.to_h
end

Expand Down Expand Up @@ -751,11 +791,7 @@ def parse_line!(line, line_num:, state:)
# @raise [FriendsError] if 0 or 2+ friends match the given text
def thing_with_name_in(type, text)
things = instance_variable_get("@#{type}s").select do |thing|
if type == :friend
thing.regexes_for_name.any? { |regex| regex.match(text) }
else
thing.regex_for_name.match(text)
end
thing.regexes_for_name.any? { |regex| regex.match(text) }
end

# If there's more than one match with fuzzy regexes but exactly one thing
Expand Down
46 changes: 39 additions & 7 deletions lib/friends/location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ class Location
extend Serializable

SERIALIZATION_PREFIX = "- "
ALIAS_PREFIX = "a.k.a. "

# @return [Regexp] the regex for capturing groups in deserialization
def self.deserialization_regex
# Note: this regex must be on one line because whitespace is important
/(#{SERIALIZATION_PREFIX})?(?<name>.+)/
/(#{SERIALIZATION_PREFIX})?(?<name>[^\(]*[^\(\s])(\s+\(#{ALIAS_PREFIX}(?<alias_str>.+)\))?/
end

# @return [Regexp] the string of what we expected during deserialization
Expand All @@ -23,21 +24,52 @@ def self.deserialization_expectation
end

# @param name [String] the name of the location
def initialize(name:)
def initialize(name:, alias_str: nil)
@name = name
@aliases = alias_str&.split(" #{ALIAS_PREFIX}") || []
end

attr_accessor :name
attr_reader :aliases

# @return [String] the file serialization text for the location
def serialize
"#{SERIALIZATION_PREFIX}#{@name}"
Paint.unpaint("#{SERIALIZATION_PREFIX}#{self}")
end

# @return [Regexp] the regex used to match this location's name in an
# activity description
def regex_for_name
Friends::RegexBuilder.regex(@name)
# @return [String] a string representing the location's name and aliases
def to_s
unless @aliases.empty?
alias_str = " (" +
@aliases.map do |nickname|
"#{ALIAS_PREFIX}#{Paint[nickname, :bold, :yellow]}"
end.join(" ") + ")"
end

"#{Paint[@name, :bold]}#{alias_str}"
end

# Add an alias, ignoring duplicates.
# @param nickname [String] the alias to add
def add_alias(nickname)
@aliases << nickname
@aliases.uniq!
end

# @param nickname [String] the alias to remove
# @raise [FriendsError] if the location does not have the given alias
def remove_alias(nickname)
unless @aliases.include? nickname
raise FriendsError, "Alias \"#{nickname}\" not found for \"#{name}\""
end

@aliases.delete(nickname)
end

# @return [Array] a list of all regexes to match the name in a string
# NOTE: Only full names and aliases
def regexes_for_name
[name, *@aliases].map { |str| Friends::RegexBuilder.regex(str) }
end

# The number of activities this location is in. This is for internal use
Expand Down
74 changes: 74 additions & 0 deletions test/commands/add/alias_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require "./test/helper"

clean_describe "add alias" do
subject { run_cmd("add alias #{location_name} #{nickname}") }

let(:content) { CONTENT }

describe "when location name and alias are blank" do
let(:location_name) { nil }
let(:nickname) { nil }

it "prints an error message" do
stderr_only 'Error: Expected "[Location Name]" "[Alias]"'
end
end

describe "when location name has no matches" do
let(:location_name) { "Garbage" }
let(:nickname) { "Big Apple Pie" }

it "prints an error message" do
stderr_only 'Error: No location found for "Garbage"'
end
end

describe "when location alias has more than one match" do
let(:location_name) { "'New York City'" }
let(:nickname) { "'Big Apple'" }
before do
run_cmd("add location Manhattan")
run_cmd("add alias Manhattan 'Big Apple'")
end

it "prints an error message" do
stderr_only "Error: The location alias "\
'"Big Apple" is already taken by "Manhattan (a.k.a. Big Apple)"'
end
end

describe "when location name has one match" do
let(:location_name) { "'New York City'" }

describe "when alias is blank" do
let(:nickname) { "' '" }

it "prints an error message" do
stderr_only "Error: Alias cannot be blank"
end
end

describe "when alias is nil" do
let(:nickname) { nil }

it "prints an error message" do
stderr_only "Error: Alias cannot be blank"
end
end

describe "when alias is not blank" do
let(:nickname) { "'Big Apple'" }

it "adds alias to location" do
line_changed "- New York City (a.k.a. NYC a.k.a. NY)",
"- New York City (a.k.a. NYC a.k.a. NY a.k.a. Big Apple)"
end

it "prints an output message" do
stdout_only 'Alias added: "New York City (a.k.a. NYC a.k.a. NY a.k.a. Big Apple)"'
end
end
end
end
1 change: 1 addition & 0 deletions test/commands/edit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
- Atlantis
- Martha's Vineyard
- Mysterious Mountains
- New York City (a.k.a. NYC a.k.a. NY)
- Paris
EXPECTED_CONTENT
end
Expand Down
14 changes: 14 additions & 0 deletions test/commands/list/locations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,21 @@
Paris
Atlantis
Martha's Vineyard
New York City
OUTPUT
end

describe "--verbose" do
subject { run_cmd("list locations --verbose") }

it "lists locations in file order with details" do
stdout_only <<-OUTPUT
Paris
Atlantis
Martha's Vineyard
New York City (a.k.a. NYC a.k.a. NY)
OUTPUT
end
end
end
end
Loading

0 comments on commit 4510bc8

Please sign in to comment.