Feel.Analysis.Grain (feel v0.1.0)

Copy Markdown View Source

How fine the numbers of a node are (D-161, step D).

A grain d of a node says that every number the node can have, null apart, is a multiple of 10^-d: d = 2 is a number of at most two decimals (a price), d = 0 a whole number, and d = -3 a multiple of a thousand. The smaller the grain, the more it says. A node with no grain (nil) says nothing. It is a fact about the node and not a part of its type, as the length of a list is (D-193): the sets of numbers are intervals, and a set of the multiples of a step is not one.

What it is for: a product or a quotient of numbers is null when its value is not zero and needs a last digit below 1E-6176 (D-59), and the sets say that this can happen for any number near zero (D-161). A number of a grain is not near zero but at a distance of 10^-d at least, so price * quantity for prices of two decimals and whole quantities cannot be too small to write. The grain comes from what a model says of a number:

  • a literal has its decimals (0.5 has 1, 150 has 0);
  • a name has the grain of its allowed values, when they say it: round half up(?, 2) = ?, floor(?) = ?, modulo(?, 5) = 0, even(?) (of_tests/1), and so do plain DMN constraints, joined by and for the least of them;
  • an operation has the grain of its operands: a sum or a difference the larger, a product their sum, a negation or an abs the same; a quotient has none;
  • a call of floor, ceiling, round * and decimal with a scale that is a number has that scale, and count, string length, day of year and week of year are whole numbers.

A grain is as true as the constraint it comes from, which the run checks before the expression runs (D-138), and Feel.Paranoid holds every node that has one to it.

iex> Feel.Analysis.Grain.of_number(Decimal.new("0.25"))
2
iex> Feel.Analysis.Grain.sum(2, 0)
2
iex> Feel.Analysis.Grain.product(2, 3)
5
iex> Feel.Analysis.Grain.multiple?(Decimal.new("1.25"), 2)
true
iex> Feel.Analysis.Grain.multiple?(Decimal.new("1.255"), 2)
false

Summary

Types

t()

A grain: every number is a multiple of 10^-d, or nil for none that is known.

Functions

Whether a number is a multiple of 10^-d.

The grain of an IR node: a literal has its decimals, and any other node has the one it was given when it was made (:grain in its metadata).

The grain of the number that a call gives, if it is known: the scale of round half up(x, 2), 0 for floor(x) and count(xs), that of the argument for abs. arguments are the IR nodes of the call.

The decimals of a number: the grain of a literal.

The grain of the numbers that unary tests accept (the allowed values of a name), if they say it. The tests are alternatives, so every one has to say it, and the grain is the coarsest of them; a test joined by and has the finest grain of any part that says one. round half up(?, 2) = ? is 2, floor(?) = ? and even(?) 0, and modulo(?, 0.25) = 0 the decimals of 0.25, which is 2.

The grain of a product: the two added.

The numbers of a set that a grain allows: each end that is not a multiple of 10^-d is brought in to the nearest one that is inside the interval, and an interval that has none is left out. [0..1E-6176] of whole numbers is 0, and (0..1] of two decimals is [0.01..1]. The ends of the set are the numbers an operation is worked out at (Feel.Analysis.Intervals), so an end that the node cannot have must not be one.

The grain of a sum or a difference: the larger of the two.

Types

t()

@type t() :: integer() | nil

A grain: every number is a multiple of 10^-d, or nil for none that is known.

Functions

multiple?(decimal, grain)

@spec multiple?(Decimal.t(), integer()) :: boolean()

Whether a number is a multiple of 10^-d.

iex> Feel.Analysis.Grain.multiple?(Decimal.new(300), -2)
true
iex> Feel.Analysis.Grain.multiple?(Decimal.new(350), -2)
false

of(node)

@spec of(Feel.IR.t()) :: t()

The grain of an IR node: a literal has its decimals, and any other node has the one it was given when it was made (:grain in its metadata).

of_call(name, arguments)

@spec of_call(String.t(), [Feel.IR.t()]) :: t()

The grain of the number that a call gives, if it is known: the scale of round half up(x, 2), 0 for floor(x) and count(xs), that of the argument for abs. arguments are the IR nodes of the call.

of_number(number)

@spec of_number(Decimal.t()) :: non_neg_integer()

The decimals of a number: the grain of a literal.

of_tests(arg1)

@spec of_tests(Feel.IR.t()) :: t()

The grain of the numbers that unary tests accept (the allowed values of a name), if they say it. The tests are alternatives, so every one has to say it, and the grain is the coarsest of them; a test joined by and has the finest grain of any part that says one. round half up(?, 2) = ? is 2, floor(?) = ? and even(?) 0, and modulo(?, 0.25) = 0 the decimals of 0.25, which is 2.

iex> {:ok, ast} = Feel.parse("? >= 0 and ? <= 150 and round half up(?, 2) = ?", entry: :unary_tests)
iex> {:ok, %{ir: tests}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.Grain.of_tests(tests)
2
iex> {:ok, ast} = Feel.parse("[0..150], round half up(?, 2) = ?", entry: :unary_tests)
iex> {:ok, %{ir: tests}} = Feel.Analysis.Analyzer.analyze(ast, input: :number, declarations: :ignore)
iex> Feel.Analysis.Grain.of_tests(tests)
nil

product(a, b)

@spec product(t(), t()) :: t()

The grain of a product: the two added.

snap(set, grain)

The numbers of a set that a grain allows: each end that is not a multiple of 10^-d is brought in to the nearest one that is inside the interval, and an interval that has none is left out. [0..1E-6176] of whole numbers is 0, and (0..1] of two decimals is [0.01..1]. The ends of the set are the numbers an operation is worked out at (Feel.Analysis.Intervals), so an end that the node cannot have must not be one.

iex> alias Feel.Type.NumberSet
iex> set = NumberSet.interval({Decimal.new(0), :open}, Decimal.new(1))
iex> set |> Feel.Analysis.Grain.snap(2) |> NumberSet.format()
"[0.01..1]"
iex> set |> Feel.Analysis.Grain.snap(0) |> NumberSet.format()
"1"
iex> NumberSet.interval(Decimal.new(0), Decimal.new("0.4")) |> Feel.Analysis.Grain.snap(0) |> NumberSet.format()
"0"

sum(a, b)

@spec sum(t(), t()) :: t()

The grain of a sum or a difference: the larger of the two.