Skip to content

Instantly share code, notes, and snippets.

@wakproductions
Created July 6, 2020 05:02
Show Gist options
  • Save wakproductions/6dc8ec8d9e6a84abbdb4a1e863eeffa0 to your computer and use it in GitHub Desktop.
Save wakproductions/6dc8ec8d9e6a84abbdb4a1e863eeffa0 to your computer and use it in GitHub Desktop.
Prettify Hash
module IndentUtil
INDENT_SIZE = 2
def indent_chars(indent_level)
' ' * INDENT_SIZE * indent_level
end
end
class PrettifyObject
MAX_LINE_SIZE = 120
def self.call(object, **options)
new.call(object, **options)
end
def call(_object, **options)
@current_indent = options[:indent] || 0
end
private
attr_reader :current_indent
# TODO - for class type items, allow passing in of "Prettify_" processing classes for special handling.
def stringify_value(value, **args)
case value
when String
(value =~ /'/).present? ? %{"#{value}"} : "'#{value}'"
when BigDecimal
"#{value}.to_d"
when Hash
PrettifyHash.call(value, indent: current_indent + 1, preceding_chars: args[:preceding_chars])
when Array
PrettifyArray.call(value, indent: current_indent + 1, preceding_chars: args[:preceding_chars])
when nil
'nil'
else
value.to_s
end
end
end
class PrettifyArray < PrettifyObject
include IndentUtil
def call(array, indent: 0, preceding_chars: '', force_wrap: false)
super
return stringify_array_as_wrapped_lines(array, indent) if force_wrap
as_one_line = stringify_array_as_one_line(array)
if string_too_long?(preceding_chars + as_one_line)
stringify_array_as_wrapped_lines(array, indent)
else
as_one_line
end
end
private
def stringify_array_as_one_line(array)
string_values =
array.map do |v|
stringify_array_value(v)
end
.join(', ')
"[#{string_values}]"
end
def stringify_array_value(value, indent_chars='')
preceding_chars = "#{indent_chars}"
"#{preceding_chars}#{stringify_value(value, preceding_chars: preceding_chars).strip}"
end
def stringify_array_as_wrapped_lines(array, indent)
base_indent = indent_chars(indent)
additional_indent = indent_chars(indent + 1)
string_values =
array.map do |v|
stringify_array_value(v, additional_indent)
end
.join(",\n")
"#{base_indent}[\n" \
"#{string_values}\n" \
"#{base_indent}]"
end
def string_too_long?(string)
string.size > MAX_LINE_SIZE
end
end
class PrettifyHash < PrettifyObject
include IndentUtil
def call(hash, indent: 0, preceding_chars: '', force_wrap: false)
super
return stringify_hash_as_wrapped_lines(hash, indent) if force_wrap
as_one_line = stringify_hash_as_one_line(hash)
if string_too_long?(preceding_chars + as_one_line)
stringify_hash_as_wrapped_lines(hash, indent)
else
as_one_line
end
end
private
def stringify_hash_as_one_line(hash)
string_values =
hash.map do |k, v|
stringify_hash_key_value_pair(k, v)
end
.join(', ')
"{ #{string_values} }"
end
def stringify_hash_key_value_pair(key, value, indent_chars='')
preceding_chars = "#{indent_chars}#{key}: "
"#{indent_chars}#{key}: #{stringify_value(value, preceding_chars: preceding_chars).strip}"
end
def stringify_hash_as_wrapped_lines(hash, indent)
base_indent = indent_chars(indent)
additional_indent = indent_chars(indent + 1)
string_values =
hash.map do |k, v|
stringify_hash_key_value_pair(k, v, additional_indent)
end
.join(",\n")
"#{base_indent}{\n" \
"#{string_values}\n" \
"#{base_indent}}"
end
def string_too_long?(string)
string.size > MAX_LINE_SIZE
end
end
require 'rails_helper'
describe PrettifyHash do
subject { described_class.call(input) }
context 'single line hash' do
let(:input) do
{ a_hash_key: 'first value', b_hash_key: 'second value', c_hash_key: 333 }
end
let(:expected_output) do
%({ a_hash_key: 'first value', b_hash_key: 'second value', c_hash_key: 333 })
end
it { is_expected.to eql(expected_output) }
end
context 'single line hash, but force first line to wrap' do
subject { described_class.call(input, force_wrap: true) }
let(:input) do
{ a_hash_key: 'first value', b_hash_key: 'second value', c_hash_key: 333 }
end
let(:expected_output) do
<<~TXT.strip
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333
}
TXT
end
it { is_expected.to eql(expected_output) }
context 'indents 1 unit' do
subject { described_class.call(input, indent: 1, force_wrap: true) }
let(:expected_output) do
%( {\n) +
%( a_hash_key: 'first value',\n) +
%( b_hash_key: 'second value',\n) +
%( c_hash_key: 333\n) +
%( })
end
it { is_expected.to eql(expected_output) }
end
context 'indents 2 units' do
subject { described_class.call(input, indent: 2, force_wrap: true) }
let(:expected_output) do
%( {\n) +
%( a_hash_key: 'first value',\n) +
%( b_hash_key: 'second value',\n) +
%( c_hash_key: 333\n) +
%( })
end
it { is_expected.to eql(expected_output) }
end
end
context 'will automatically wrap when single line exceeds 120 characters' do
subject { described_class.call(input) }
let(:input) do
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333,
d_hash_key: 4_444_555,
e_hash_key: 'this is a really really really long string',
f_hash_key: 'this is another really really long string'
}
end
let(:expected_output) do
<<~TXT.strip
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333,
d_hash_key: 4444555,
e_hash_key: 'this is a really really really long string',
f_hash_key: 'this is another really really long string'
}
TXT
end
it { is_expected.to eql(expected_output) }
context 'with 1 level of nesting' do
let(:input) do
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333,
d_hash_key: 4_444_555,
e_hash_key: 'this is a really really really long string',
f_hash_key: 'this is another really really long string',
g_hash_key: {
nested_hash_key_1: 'this',
nested_hash_key_2: 'is',
nested_hash_key_3: 'going',
nested_hash_key_4: 'to',
nested_hash_key_5: 'have',
nested_hash_key_6: 'lots',
nested_hash_key_7: 'of',
nested_hash_key_8: 'wrap'
},
h_hash_key: { nowrap: "this won't wrap" }
}
end
let(:expected_output) do
<<~TXT.strip
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333,
d_hash_key: 4444555,
e_hash_key: 'this is a really really really long string',
f_hash_key: 'this is another really really long string',
g_hash_key: {
nested_hash_key_1: 'this',
nested_hash_key_2: 'is',
nested_hash_key_3: 'going',
nested_hash_key_4: 'to',
nested_hash_key_5: 'have',
nested_hash_key_6: 'lots',
nested_hash_key_7: 'of',
nested_hash_key_8: 'wrap'
},
h_hash_key: { nowrap: "this won't wrap" }
}
TXT
end
it { is_expected.to eql(expected_output) }
context 'with nested value is < 120 chars, but exceeds with indent' do
let(:input) do
{
a_hash_key: 'first value',
b_hash_key: 'second value',
long_hash_key: { n_hash_key_1: 'this', n_hash_key_2: 'text', nested_hash_key_2: 'should', nested_hash_key_3: 'wrap..' },
h_hash_key: { nowrap: "this won't wrap because the total string length including indent is equal to 120 characters." }
}
end
let(:expected_output) do
<<~TXT.strip
{
a_hash_key: 'first value',
b_hash_key: 'second value',
long_hash_key: {
n_hash_key_1: 'this',
n_hash_key_2: 'text',
nested_hash_key_2: 'should',
nested_hash_key_3: 'wrap..'
},
h_hash_key: { nowrap: "this won't wrap because the total string length including indent is equal to 120 characters." }
}
TXT
end
it { is_expected.to eql(expected_output) }
end
end
context 'with 2 levels of nesting' do
let(:input) do
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333,
d_hash_key: 4_444_555,
e_hash_key: 'this is a really really really long string',
f_hash_key: 'this is another really really long string',
g_hash_key: {
nested_hash_key_1: 'this',
nested_hash_key_2: 'is',
nested_hash_key_3: 'going',
nested_hash_key_4: 'to',
nested_hash_key_5: 'have',
nested_hash_key_6: 'lots',
nested_hash_key_7: 'of',
nested_hash_key_8: 'wrap',
nested_hash_key_really_long: {
nested2_key_1: 'this',
nested2_key_2: 'is',
nested2_key_3: 'another',
nested2_key_4: 'long',
nested2_key_5: 'nested',
nested2_key_6: 'hash',
},
array_hash_key_oneline: [
{ array_hash_key_1: 'just numbers', array_hash_key_2: 3456 },
'this is a string value',
],
array_hash_key_multiline: [
{ array_hash_key_1: 'this one has a long string', array_hash_key_2: 555555 },
"it's going to be an array going multiple lines",
6545645
]
},
h_hash_key: { nowrap: "this won't wrap" }
}
end
let(:expected_output) do
<<~TXT.strip
{
a_hash_key: 'first value',
b_hash_key: 'second value',
c_hash_key: 333,
d_hash_key: 4444555,
e_hash_key: 'this is a really really really long string',
f_hash_key: 'this is another really really long string',
g_hash_key: {
nested_hash_key_1: 'this',
nested_hash_key_2: 'is',
nested_hash_key_3: 'going',
nested_hash_key_4: 'to',
nested_hash_key_5: 'have',
nested_hash_key_6: 'lots',
nested_hash_key_7: 'of',
nested_hash_key_8: 'wrap',
nested_hash_key_really_long: {
nested2_key_1: 'this',
nested2_key_2: 'is',
nested2_key_3: 'another',
nested2_key_4: 'long',
nested2_key_5: 'nested',
nested2_key_6: 'hash'
},
array_hash_key_oneline: [{ array_hash_key_1: 'just numbers', array_hash_key_2: 3456 }, 'this is a string value'],
array_hash_key_multiline: [
{ array_hash_key_1: 'this one has a long string', array_hash_key_2: 555555 },
"it's going to be an array going multiple lines",
6545645
]
},
h_hash_key: { nowrap: "this won't wrap" }
}
TXT
end
it { is_expected.to eql(expected_output) }
end
end
context '1 level nesting' do
let(:input) do
{
car: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 },
truck: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 },
suv: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 }
}
end
let(:expected_output) do
<<~TXT.strip
{
car: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 },
truck: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 },
suv: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 }
}
TXT
end
it { is_expected.to eql(expected_output) }
end
context 'multiple levels nesting' do
let(:input) do
{
car: {
material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0, special_cost: 4.0, additional_cost: 5.0,
nested_cost: {
nested_level_2: true,
property_detail_level_2: BigDecimal('12345.6789'),
long_text_property: 'this is a text string that should wrap onto a new line'
},
utility_cost: 6.0,
compliance_cost: 7.0
},
truck: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 },
suv: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 }
}
end
let(:expected_output) do
<<~TXT.strip
{
car: {
material_cost: 1.0,
labor_cost: 2.0,
profit_cost: 3.0,
special_cost: 4.0,
additional_cost: 5.0,
nested_cost: {
nested_level_2: true,
property_detail_level_2: 12345.6789.to_d,
long_text_property: 'this is a text string that should wrap onto a new line'
},
utility_cost: 6.0,
compliance_cost: 7.0
},
truck: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 },
suv: { material_cost: 1.0, labor_cost: 2.0, profit_cost: 3.0 }
}
TXT
end
it { is_expected.to eql(expected_output) }
end
end
@TSMMark
Copy link

TSMMark commented Dec 11, 2023

Could you publish this as a gem?

@wakproductions
Copy link
Author

@TSMMark I'm glad you found this useful! I don't have time to get to it right away. If you wanna package it, go ahead. I'd be interested in seeing the repo. The way I use it right now is I put it in a file in a gitignored directory, and when I want to use it in Rails console, I call require 'gitignore/prettify_hash.rb and then it makes the class available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment