Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: decimal fraction #68

Merged
merged 2 commits into from Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 85 additions & 0 deletions doc/dial.jax
Expand Up @@ -20,6 +20,7 @@ Configurations |dial-config|
Augends |dial-augends|
Number |dial-augends-number|
Date |dial-augends-date|
Decimal Fraction |dial-augends-decimal-fraction|
Constant |dial-augends-constant|
Case |dial-augends-case|
Hexcolor |dial-augends-hexcolor|
Expand Down Expand Up @@ -868,6 +869,90 @@ augend.date.alias["%H:%M"]~
がインクリメントされます。
意図しないマッチを避けるため、存在しない時刻にはマッチしません。

DECIMAL FRACTION *dial-augends-decimal-fraction*
----------------

小数を増減します。
`augend.decimal_fraction.new{ ...opts }` で使用できます。
>
require("dial.config").augends:register_group{
default = {
augend.decimal_fraction.new{
signed = false,
point_char = ".",
},
},
}
<

以下のパラメータを有します。
signed (boolean, default: false)
true のとき、小数に前置されたマイナスを加味します。
point_char (string, default: `.`)
小数点を表す文字。1文字である必要があります。基本的には `.` か
`,` のどちらかです。

decimal_fraction は `42.195` や `9.80665` といった小数を見つけたとき、カーソル
が整数部分の前にあれば整数部分を、小数部分の前にあれば小数部分を増減します。

たとえば
>
42.195
<
という文字列の `4` にカーソルがある状態で |<Plug>(dial-increment)| を行うと
>
43.195
<
となり、さらに`1` にカーソルがある状態で |<Plug>(dial-increment)| を行うと
>
43.196
<
となります。カウンタを指定することもでき、小数部分にカーソルを当てて
|100<Plug>(dial-increment)| を実行すれば
>
43.296
<
となります。

上記の挙動だけ見ると integer を指定したときの挙動とほとんど変わらないと感じら
れるかも知れません。しかし、integer とは以下の2つの違いがあります。

* 繰り上がりや繰り下がりが発生します。すなわち
>
2.9
<
の小数部分にカーソルを当てて |<Plug>(dial-increment)| を行うと、
>
3.0
<
となります(integer を指定したときは `2.10` となっていました)。
これは減算でも同様です。 `signed = true` の設定で
>
0.7
<
の整数部分にカーソルを当てて |<Plug>(dial-decrement)| を行うと、
>
-0.3
<
となります (`0.7 - 1 = -0.3`)。

* ドットリピートは「増減する実際の値」を記憶し、再現します。`2.8` の小数部分を
インクリメントして `2.9` にした直後なら、 `1.73` をドットリピートでインクリ
メントしたときの結果は `1.74` ではなく `1.83` となります。

`augend.decimal_fraction` と `augend.integer` を両立することもできます。
>
require("dial.config").augends:register_group{
default = {
augend.integer.alias.decimal,
augend.decimal_fraction.new{ },
},
}
<

この場合はカーソル下または直後にある数字が小数であるときに限り、
decimal_fraction による増減ルールが適用されます。

CONSTANT *dial-augends-constant*
--------

Expand Down
82 changes: 82 additions & 0 deletions doc/dial.txt
Expand Up @@ -21,6 +21,7 @@ Configurations |dial-config|
Augends |dial-augends|
Number |dial-augends-number|
Date |dial-augends-date|
Decimal Fraction |dial-augends-decimal-fraction|
Constant |dial-augends-constant|
Case |dial-augends-case|
Hexcolor |dial-augends-hexcolor|
Expand Down Expand Up @@ -904,6 +905,87 @@ target of the increment.
The format matches times that do not exist in reality such as `52:99`, and
the time is corrected to a date that actually exists when incrementing.

DECIMAL FRACTION *dial-augends-decimal-fraction*
----------------

Represents decimal fraction number, such as `42.195` and `-9.80665`.
`augend.decimal_fraction.new{ ...opts }` で使用できます。
>
require("dial.config").augends:register_group{
default = {
augend.decimal_fraction.new{
signed = false,
point_char = ".",
},
},
}
<

The argument table of `augend.decimal_fraction.new` can take the following
keys:
signed (boolean, default: false)
If true, add the minus prefixed to the decimal.
point_char (string, default: `.`)
A decimal point character, which must be a single character.
Either "." or "," in most cases.

When decimal_fraction augend finds a fraction such as `42.195` or `9.80665`,
it increments/decrements:
* the integer part if the cursor is on/before the integer part,
* the fractional part if the cursor is on/before the fractional part.

For instance, if you perform |<Plug>(dial-increment)| with the cursor at `4`
of the following string:
>
42.195
<
then you will get the following result.
>
43.195
<
And if you perform |<Plug>(dial-increment)| with the cursor at `1` in
succession, you will get the following result.
>
43.196
<
You can specify the [count]. By performing |100<Plug>(dial-increment)| on the
fractional part, you will get:
>
43.296
<

You may feel that the behavior is almost the same as when an integer is
specified. However, there are two important differences from integer:

* the value is carried across the decimal point. e.g., if you perform
|<Plug>(dial-increment)| on the fractional part of the following:
>
2.9
<
you will get:
>
3.0
<
If you had specified `integer` augend, you would have got `2.10`.

* Dot repeat remembers and reproduces the "actual value to be increased or
decreased." Immediately after incrementing the decimal portion of `2.8`
to `2.9,` the result of dot repeat incrementing `1.73` would be `1.83`,
not `1.74`.

You can also use both `augend.decimal_fraction` and `augend.integer`:
>
require("dial.config").augends:register_group{
default = {
augend.integer.alias.decimal,
augend.decimal_fraction.new{ },
},
}
<

In this case, the increment/decrement rule by `decimal_fraction` is applied
only when the number after/on the cursor is a decimal fraction number.

CONSTANT *dial-augends-constant*
--------

Expand Down
2 changes: 2 additions & 0 deletions lua/dial/augend.lua
@@ -1,6 +1,7 @@
local case = require "dial.augend.case"
local constant = require "dial.augend.constant"
local date = require "dial.augend.date"
local decimal_fraction = require "dial.augend.decimal_fraction"
local hexcolor = require "dial.augend.hexcolor"
local integer = require "dial.augend.integer"
local semver = require "dial.augend.semver"
Expand All @@ -12,6 +13,7 @@ return {
case = case,
constant = constant,
date = date,
decimal_fraction = decimal_fraction,
hexcolor = hexcolor,
integer = integer,
semver = semver,
Expand Down
150 changes: 150 additions & 0 deletions lua/dial/augend/decimal_fraction.lua
@@ -0,0 +1,150 @@
local common = require "dial.augend.common"
local util = require "dial.util"

---@class AugendDecimalFraction
---@implement Augend
---@field signed boolean
---@field point_char string
local AugendDecimalFraction = {}

local M = {}

---@param config { signed?: boolean, point_char?: string }
---@return Augend
function M.new(config)
vim.validate {
signed = { config.signed, "boolean", true },
point_char = { config.point_char, "string", true },
}

local signed = util.unwrap_or(config.signed, false)
local point_char = util.unwrap_or(config.point_char, ".")
local digits_to_add = 0

return setmetatable({
signed = signed,
point_char = point_char,
digits_to_add = digits_to_add,
}, { __index = AugendDecimalFraction })
end

---@param line string
---@param cursor? integer
---@return textrange?
function AugendDecimalFraction:find(line, cursor)
local idx = 1
local integer_pattern
if self.signed then
integer_pattern = "%-?%d+"
else
integer_pattern = "%d+"
end
while idx <= #line do
local idx_integer_start, idx_integer_end = line:find(integer_pattern, idx)
if idx_integer_start == nil then
break
end

local result = (function()
idx = idx_integer_end + 1
-- invalid decimal fraction format
if line:sub(idx, idx) ~= self.point_char then
return -- continue while loop
end
idx = idx + 1
local idx_frac_start, idx_frac_end = line:find("^%d+", idx)
-- invalid decimal fraction format
if idx_frac_start == nil then
return -- continue while loop
end
idx = idx_frac_end + 1
-- decimal fraction before the cursor
if idx_frac_end < cursor then
return -- continue while loop
end

-- negative lookahead
if line:sub(idx, idx) == self.point_char then
return -- continue while loop
end
-- break loop and return value
return { from = idx_integer_start, to = idx_frac_end }
end)()

if result ~= nil then
return result
end
end
end

---@param line string
---@param cursor? integer
---@return textrange?
function AugendDecimalFraction:find_stateful(line, cursor)
local result = self:find(line, cursor)
if result == nil then
return nil
end

local point_pos = line:find(self.point_char, result.from, true)
if cursor < point_pos then
-- increment integer part
self.digits_to_add = 0
else
-- increment decimal part
self.digits_to_add = result.to - point_pos
end
return result
end

---@param text string
---@param addend integer
---@param cursor? integer
---@return { text?: string, cursor?: integer }
function AugendDecimalFraction:add(text, addend, cursor)
local point_pos = text:find(self.point_char, 1, true)

local int_part = text:sub(1, point_pos - 1)
local frac_part = text:sub(point_pos + 1)

-- 桁数調整。元の数字が 12.3 なのに 0.01 を足したいとき、 12.31 になるようにする
if #frac_part < self.digits_to_add then
frac_part = frac_part .. ("0"):rep(self.digits_to_add - #frac_part)
end

local num = tonumber(int_part .. frac_part)
local add_num = addend * math.floor(10 ^ (#frac_part - self.digits_to_add))
num = num + add_num
if not self.signed and num < 0 then
num = 0
end
local str_num = tostring(num)

if num < 0 then
if #str_num - 1 <= #frac_part then
str_num = "-" .. ("0"):rep(#frac_part + 2 - #str_num) .. str_num:sub(2)
end
else
if #str_num <= #frac_part then
str_num = ("0"):rep(#frac_part + 1 - #str_num) .. str_num
end
end

-- pad as necessary
local new_int_part = str_num:sub(1, #str_num - #frac_part)
local new_dec_part = str_num:sub(#str_num - #frac_part + 1)

text = new_int_part .. "." .. new_dec_part
if self.digits_to_add == 0 then
-- incremented integer part
cursor = #new_int_part
else
cursor = #text
end

return { text = text, cursor = cursor }
end

M.alias = {}

return M