| locale | en | ||
|---|---|---|---|
| tags |
|
immosquare-extensions
immosquare-extensions is a Ruby gem that adds utility methods to the core classes String, Hash, Array and File, plus a nested-attribute accessor on Rails ApplicationRecord. This README is for Ruby and Rails developers using the gem: it covers installation, every method added class by class, and how to run the test suite and the CI entry point. It assumes Ruby >= 3.2.6; String#titleize_place and String#titleize_name additionally need ActiveSupport and File.normalize_last_line the uchardet CLI binary.
- Installing immosquare-extensions
- String extensions
- Hash extensions
- Array extensions
- File extensions
- ApplicationRecord extensions
- Developing immosquare-extensions
- Contributing and license
Installing immosquare-extensions
Add this line to your Gemfile:
gem "immosquare-extensions"Then run:
bundle installRequires Ruby >= 3.2.6. File.normalize_last_line additionally requires the uchardet CLI binary (brew install uchardet) for encoding detection.
String extensions: to_boolean, titleize_place and titleize_name
immosquare-extensions adds three methods to String.
String#to_boolean converts "true" and "false" strings to boolean values. Returns nil (or a default value) for other strings.
"true".to_boolean # => true
"false".to_boolean # => false
"TRUE".to_boolean # => true (case-insensitive)
"other".to_boolean # => nil
# With default value
"other".to_boolean(true) # => true
"other".to_boolean("default") # => "default"String#titleize_place titleizes a place name under French typographic rules, which standard titleize gets wrong: hyphens are preserved, the capital goes on the letter after an apostrophe, and particles (de, du, des, la, le, les, sur, en, ès, à, au, aux, et, sous, lès) stay lowercase unless they open the name. An English possessive is left alone. It requires ActiveSupport (available in Rails applications).
"SANT-ANDREA-D'ORCINO".titleize_place # => "Sant-Andrea-d'Orcino"
"saint-jean-sur-richelieu".titleize_place # => "Saint-Jean-sur-Richelieu"
"l'assomption".titleize_place # => "L'Assomption"
"MONTRÉAL".titleize_place # => "Montréal"
"st john's".titleize_place # => "St John's"
"hello world".titleize_place # => "Hello World"String#titleize_name titleizes anything that is not a place — a person, an agency, a building, an enumeration label. Same apostrophe and accent handling as titleize_place, but no particle is lowercased.
"o'brien".titleize_name # => "O'Brien"
"MAISON DE VILLE".titleize_name # => "Maison De Ville"
"émilie".titleize_name # => "Émilie"Both methods normalize through ActiveSupport's humanize first, which has three known consequences: internal capitals are flattened (MacDonald → Macdonald), a dotted abbreviation reads as a particle (n.-d.-de-grâce → N.-d.-de-Grâce) and roman numerals are recapitalized (Louis XIV → Louis Xiv).
titleize_placeandtitleize_namereplacetitleize_custom, removed in0.2.0.titleize_customcapitalized the letter before the apostrophe and lowered no particle, so"SANT-ANDREA-D'ORCINO"came out as"Sant-Andrea-D'orcino".
Hash extensions: without, depth, sort_by_key, flatten_hash and to_beautiful_json
immosquare-extensions adds five methods to Hash.
Hash#without removes multiple keys from a hash in a single operation.
{a: 1, b: 2, c: 3}.without(:a, :b) # => {c: 3}
{a: 1, b: 2}.without(:x) # => {a: 1, b: 2} (non-existent keys ignored)Hash#depth returns the nesting depth of a hash.
{a: 1}.depth # => 1
{a: {b: 1}}.depth # => 2
{a: {b: {c: 1}}}.depth # => 3
{}.depth # => 0Hash#sort_by_key sorts a hash by its keys. Optionally sorts nested hashes recursively. Sorting is case-insensitive.
{b: 1, a: 2}.sort_by_key
# => {a: 2, b: 1}
{b: 1, a: {d: 4, c: 3}}.sort_by_key
# => {a: {c: 3, d: 4}, b: 1}
# Without recursion
{b: 1, a: {d: 4, c: 3}}.sort_by_key(recursive: false)
# => {a: {d: 4, c: 3}, b: 1}
# With custom sorting block
{b: 1, a: 2, c: 3}.sort_by_key { |x, y| y <=> x }
# => {c: 3, b: 1, a: 2}Hash#flatten_hash flattens a nested hash into a single-level hash with dot notation keys.
{a: {b: {c: 1}}}.flatten_hash
# => {:"a.b.c" => 1}
{a: 1, b: {c: 2, d: 3}}.flatten_hash
# => {a: 1, :"b.c" => 2, :"b.d" => 3}Hash#to_beautiful_json renders the hash as a formatted JSON string with aligned colons and customizable indentation. Its two options, with their default values and what each one controls:
| Option | Default | Description |
|---|---|---|
align |
true |
Aligns colons in key-value pairs |
indent_size |
2 |
Number of spaces per indentation level. Values <= 0 or > 10 fall back to the default 2
|
hash = {
name: "John",
age: 30,
address: {
street: "123 Apple St",
city: "FruitVille"
},
active: true,
scores: [85, 90, 78]
}
puts hash.to_beautiful_jsonOutput (aligned):
{
"name": "John",
"age": 30,
"address": {
"street": "123 Apple St",
"city": "FruitVille"
},
"active": true,
"scores": [
85,
90,
78
]
}Without alignment:
puts hash.to_beautiful_json(align: false){
"name": "John",
"age": 30,
"address": {
"street": "123 Apple St",
"city": "FruitVille"
},
"active": true,
"scores": [
85,
90,
78
]
}Array extensions: mean and to_beautiful_json
immosquare-extensions adds two methods to Array.
Array#mean calculates the arithmetic mean (average) of numerical arrays.
[1, 2, 3, 4, 5].mean # => 3.0
[2, 4, 6].mean # => 4.0
[10].mean # => 10.0
[].mean # => NaN (division by zero)Array#to_beautiful_json renders the array as a formatted JSON string, with the same options as Hash#to_beautiful_json.
data = [
{name: "Alice", age: 25},
{name: "Bob", age: 30}
]
puts data.to_beautiful_jsonOutput:
[
{
"name": "Alice",
"age": 25
},
{
"name": "Bob",
"age": 30
}
]File extensions: normalize_last_line
File.normalize_last_line ensures a file ends with exactly one newline character. Removes trailing empty lines and adds a newline if missing. Returns the total number of lines (or 0 if the file is empty).
The file encoding is auto-detected via the uchardet CLI binary and preserved on write. Detected encodings outside a known whitelist (UTF-8/16/32, Windows-125x, ISO-8859-x, KOI8-R, Big5, GB2312, Shift_JIS, EUC-JP/KR, ISO-2022-JP/KR/CN) fall back to UTF-8. File paths containing spaces are supported.
# File content: "line1\nline2\nline3" (no trailing newline)
File.normalize_last_line("path/to/file.txt")
# File content becomes: "line1\nline2\nline3\n"
# => 3
# File content: "line1\nline2\n\n\n" (multiple trailing newlines)
File.normalize_last_line("path/to/file.txt")
# File content becomes: "line1\nline2\n"
# => 2ApplicationRecord extensions in Rails: dig
The ApplicationRecord extensions of immosquare-extensions are automatically included in ActiveRecord::Base when using Rails.
dig accesses nested attributes on ActiveRecord models without manual nil checks. Returns nil if any intermediate value is missing.
user = User.first
# Instead of:
user.profile&.card_type&.slug
# You can write:
user.dig(:profile, :card_type, :slug) # => "premium"
user.dig(:profile, :missing, :slug) # => nilDeveloping immosquare-extensions: test suite, coverage and CI
To work on immosquare-extensions itself, install the dependencies and run the suite:
bundle install
bundle exec rspecSet COVERAGE=true to measure coverage. Without it, spec/coverage_helper.rb is a no-op: the run stays fast and leaves no coverage/ directory behind.
COVERAGE=true bundle exec rspecCoverage is written to coverage/ in two formats — an HTML report and coverage/lcov.info, which is the file the CI reads.
bin/ci is the entry point used by the CI, and it works the same on a laptop. Each row below is one of its subcommands and what it runs:
| Command | What it does |
|---|---|
bin/ci init |
Installs the bundle without the development group |
bin/ci test |
Runs bundle exec rspec
|
bin/ci |
Both, in that order (the default, all) |
The script provisions nothing itself: the CI runner selects the Ruby pinned in .ruby-version and the gemset named by .ruby-gemset before calling it. It defaults COVERAGE to true, and the CI collects coverage/lcov.info.
Dependencies the specs need go in the test group of the Gemfile, never in development: the CI exports BUNDLE_WITHOUT="development" and a gem placed there is missing at test time.
Contributing to immosquare-extensions and license
Bug reports and pull requests are welcome on GitHub at https://github.com/immosquare/immosquare-extensions.
The gem is available as open-source under the terms of the MIT License.