diff --git a/CHANGELOG.md b/CHANGELOG.md index b2617369153..1bc669701cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [#7862](https://github.com/rubocop-hq/rubocop/issues/7862): Corrector now has a `wrap` method. ([@marcandre][]) * [#7850](https://github.com/rubocop-hq/rubocop/issues/7850): Make it possible to enable/disable pending cops. ([@koic][]) * [#7861](https://github.com/rubocop-hq/rubocop/issues/7861): Make it to allow `Style/CaseEquality` when the receiver is a constant. ([@rafaelfranca][]) +* Add a new `Style/ExponentialNotation` cop. ([@tdeo][]) ### Bug fixes diff --git a/config/default.yml b/config/default.yml index a8a81e721cf..3feb5c1a6b8 100644 --- a/config/default.yml +++ b/config/default.yml @@ -2757,6 +2757,17 @@ Style/ExpandPathArguments: Enabled: true VersionAdded: '0.53' +Style/ExponentialNotation: + Description: 'When using exponential notation, favor a mantissa between 1 (inclusive) and 10 (exclusive).' + StyleGuide: '#exponential-notation' + Enabled: pending + VersionAdded: '0.82' + EnforcedStyle: scientific + SupportedStyles: + - scientific + - engineering + - integral + Style/FloatDivision: Description: 'For performing float division, coerce one side only.' StyleGuide: '#float-division' diff --git a/lib/rubocop.rb b/lib/rubocop.rb index 60717410ba9..dc7ff847b1b 100644 --- a/lib/rubocop.rb +++ b/lib/rubocop.rb @@ -441,6 +441,7 @@ require_relative 'rubocop/cop/style/eval_with_location' require_relative 'rubocop/cop/style/even_odd' require_relative 'rubocop/cop/style/expand_path_arguments' +require_relative 'rubocop/cop/style/exponential_notation' require_relative 'rubocop/cop/style/float_division' require_relative 'rubocop/cop/style/for' require_relative 'rubocop/cop/style/format_string' diff --git a/lib/rubocop/cop/style/exponential_notation.rb b/lib/rubocop/cop/style/exponential_notation.rb new file mode 100644 index 00000000000..45a62bf1bf0 --- /dev/null +++ b/lib/rubocop/cop/style/exponential_notation.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Style + # This cop enforces consistency when using exponential notation + # for numbers in the code (eg 1.2e4). Different styles are supported: + # - `scientific` which enforces a mantissa between 1 (inclusive) + # and 10 (exclusive). + # - `engineering` which enforces the exponent to be a multiple of 3 + # and the mantissa to be between 0.1 (inclusive) + # and 10 (exclusive). + # - `integral` which enforces the mantissa to always be a whole number + # without trailing zeroes. + # + # @example EnforcedStyle: scientific (default) + # # Enforces a mantissa between 1 (inclusive) and 10 (exclusive). + # + # # bad + # 10e6 + # 0.3e4 + # 11.7e5 + # 3.14e0 + # + # # good + # 1e7 + # 3e3 + # 1.17e6 + # 3.14 + # + # @example EnforcedStyle: engineering + # # Enforces using multiple of 3 exponents, + # # mantissa should be between 0.1 (inclusive) and 1000 (exclusive) + # + # # bad + # 3.2e7 + # 0.1e5 + # 12e5 + # 1232e6 + # + # # good + # 32e6 + # 10e3 + # 1.2e6 + # 1.232e9 + # + # @example EnforcedStyle: integral + # # Enforces the mantissa to have no decimal part and no + # # trailing zeroes. + # + # # bad + # 3.2e7 + # 0.1e5 + # 120e4 + # + # # good + # 32e6 + # 1e4 + # 12e5 + # + class ExponentialNotation < Cop + include ConfigurableEnforcedStyle + + def on_float(node) + add_offense(node) if offense?(node) + end + + private + + def scientific?(node) + mantissa, = node.source.split('e') + mantissa =~ /^-?[1-9](\.\d*[0-9])?$/ + end + + def engineering?(node) + mantissa, exponent = node.source.split('e') + return false unless exponent =~ /^-?\d+$/ + return false unless (exponent.to_i % 3).zero? + return false if mantissa =~ /^-?\d{4}/ + return false if mantissa =~ /^-?0\d/ + return false if mantissa =~ /^-?0.0/ + + true + end + + def integral(node) + mantissa, = node.source.split('e') + mantissa =~ /^-?[1-9](\d*[1-9])?$/ + end + + def offense?(node) + return false unless node.source['e'] + + case style + when :scientific + !scientific?(node) + when :engineering + !engineering?(node) + when :integral + !integral(node) + else + false + end + end + + def message(_node) + case style + when :scientific + 'Use a mantissa in [1, 10[.' + when :engineering + 'Use an exponent divisible by 3 and a mantissa in [0.1, 1000[.' + when :integral + 'Use an integer as mantissa, without trailing zero.' + end + end + end + end + end +end diff --git a/manual/cops.md b/manual/cops.md index aafcfc2968e..a8f56fb4fb6 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -352,6 +352,7 @@ In the following section you find all available cops: * [Style/EvalWithLocation](cops_style.md#styleevalwithlocation) * [Style/EvenOdd](cops_style.md#styleevenodd) * [Style/ExpandPathArguments](cops_style.md#styleexpandpatharguments) +* [Style/ExponentialNotation](cops_style.md#styleexponentialnotation) * [Style/FloatDivision](cops_style.md#stylefloatdivision) * [Style/For](cops_style.md#stylefor) * [Style/FormatString](cops_style.md#styleformatstring) diff --git a/manual/cops_style.md b/manual/cops_style.md index e654fd2443d..ec4fe0222cb 100644 --- a/manual/cops_style.md +++ b/manual/cops_style.md @@ -2101,6 +2101,86 @@ Pathname.new(__FILE__).parent.expand_path Pathname.new(__dir__).expand_path ``` +## Style/ExponentialNotation + +Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged +--- | --- | --- | --- | --- +Pending | Yes | No | 0.82 | - + +This cop enforces consistency when using exponential notation +for numbers in the code (eg 1.2e4). Different styles are supported: +- `scientific` which enforces a mantissa between 1 (inclusive) + and 10 (exclusive). +- `engineering` which enforces the exponent to be a multiple of 3 + and the mantissa to be between 0.1 (inclusive) + and 10 (exclusive). +- `integral` which enforces the mantissa to always be a whole number + without trailing zeroes. + +### Examples + +#### EnforcedStyle: scientific (default) + +```ruby +# Enforces a mantissa between 1 (inclusive) and 10 (exclusive). + +# bad +10e6 +0.3e4 +11.7e5 +3.14e0 + +# good +1e7 +3e3 +1.17e6 +3.14 +``` +#### EnforcedStyle: engineering + +```ruby +# Enforces using multiple of 3 exponents, +# mantissa should be between 0.1 (inclusive) and 1000 (exclusive) + +# bad +3.2e7 +0.1e5 +12e5 +1232e6 + +# good +32e6 +10e3 +1.2e6 +1.232e9 +``` +#### EnforcedStyle: integral + +```ruby +# Enforces the mantissa to have no decimal part and no +# trailing zeroes. + +# bad +3.2e7 +0.1e5 +120e4 + +# good +32e6 +1e4 +12e5 +``` + +### Configurable attributes + +Name | Default value | Configurable values +--- | --- | --- +EnforcedStyle | `scientific` | `scientific`, `engineering`, `integral` + +### References + +* [https://rubystyle.guide#exponential-notation](https://rubystyle.guide#exponential-notation) + ## Style/FloatDivision Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged diff --git a/spec/rubocop/cop/style/exponential_notation_spec.rb b/spec/rubocop/cop/style/exponential_notation_spec.rb new file mode 100644 index 00000000000..e29098371e2 --- /dev/null +++ b/spec/rubocop/cop/style/exponential_notation_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Style::ExponentialNotation, :config do + subject(:cop) { described_class.new(config) } + + context 'EnforcedStyle is scientific' do + let(:cop_config) { { 'EnforcedStyle' => 'scientific' } } + + it 'registers an offense for mantissa equal to 10' do + expect_offense(<<~RUBY) + 10e6 + ^^^^ Use a mantissa in [1, 10[. + RUBY + end + + it 'registers an offense for mantissa greater than 10' do + expect_offense(<<~RUBY) + 12.34e3 + ^^^^^^^ Use a mantissa in [1, 10[. + RUBY + end + + it 'registers an offense for mantissa smaller than 1' do + expect_offense(<<~RUBY) + 0.314e1 + ^^^^^^^ Use a mantissa in [1, 10[. + RUBY + end + + it 'registers no offense for a regular float' do + expect_no_offenses('120.03') + end + + it 'registers no offense for a float smaller than 1' do + expect_no_offenses('0.07390') + end + + it 'registers no offense for a mantissa equal to 1' do + expect_no_offenses('1e6') + end + + it 'registers no offense for a mantissa between 1 and 10' do + expect_no_offenses('3.1415e3') + end + + it 'registers no offense for a negative mantissa' do + expect_no_offenses('-9.999e3') + end + + it 'registers no offense for a negative exponent' do + expect_no_offenses('5.02e-3') + end + end + + context 'EnforcedStyle is engineering' do + let(:cop_config) { { 'EnforcedStyle' => 'engineering' } } + + it 'registers an offense for exponent equal to 4' do + expect_offense(<<~RUBY) + 10e4 + ^^^^ Use an exponent divisible by 3 and a mantissa in [0.1, 1000[. + RUBY + end + + it 'registers an offense for exponent equal to -2' do + expect_offense(<<~RUBY) + 12.3e-2 + ^^^^^^^ Use an exponent divisible by 3 and a mantissa in [0.1, 1000[. + RUBY + end + + it 'registers an offense for mantissa smaller than 0.1' do + expect_offense(<<~RUBY) + 0.09e9 + ^^^^^^ Use an exponent divisible by 3 and a mantissa in [0.1, 1000[. + RUBY + end + + it 'registers an offense for a mantissa greater than -0.1' do + expect_offense(<<~RUBY) + -0.09e3 + ^^^^^^^ Use an exponent divisible by 3 and a mantissa in [0.1, 1000[. + RUBY + end + + it 'registers an offense for mantissa smaller than -1000' do + expect_offense(<<~RUBY) + -1012.34e6 + ^^^^^^^^^^ Use an exponent divisible by 3 and a mantissa in [0.1, 1000[. + RUBY + end + + it 'registers no offense for a mantissa equal to 1' do + expect_no_offenses('1e6') + end + + it 'registers no offense for a regular float' do + expect_no_offenses('120.03') + end + + it 'registers no offense for a float smaller than 1' do + expect_no_offenses('0.07390') + end + + it 'registers no offense for a negative exponent' do + expect_no_offenses('3.1415e-12') + end + + it 'registers no offense for a negative mantissa' do + expect_no_offenses('-999.9e3') + end + + it 'registers no offense for a large mantissa' do + expect_no_offenses('968.64982e12') + end + end + + context 'EnforcedStyle is integral' do + let(:cop_config) { { 'EnforcedStyle' => 'integral' } } + + it 'registers an offense for decimal mantissa' do + expect_offense(<<~RUBY) + 1.2e3 + ^^^^^ Use an integer as mantissa, without trailing zero. + RUBY + end + + it 'registers an offense for mantissa divisible by 10' do + expect_offense(<<~RUBY) + 120e-4 + ^^^^^^ Use an integer as mantissa, without trailing zero. + RUBY + end + + it 'registers no offense for a regular float' do + expect_no_offenses('120.03') + end + + it 'registers no offense for a float smaller than 1' do + expect_no_offenses('0.07390') + end + + it 'registers no offense for an integral mantissa' do + expect_no_offenses('7652e7') + end + + it 'registers no offense for negative mantissa' do + expect_no_offenses('-84e7') + end + + it 'registers no offense for negative exponent' do + expect_no_offenses('84e-7') + end + end +end