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

Manged decimal #172

Merged
merged 9 commits into from
Jan 14, 2025
Merged

Manged decimal #172

merged 9 commits into from
Jan 14, 2025

Conversation

popenta
Copy link
Collaborator

@popenta popenta commented Dec 20, 2024

The integration test requires localnet. To set up a localnet, check out the documentation.

The implementation was inspired by this PR.

Copy link

github-actions bot commented Dec 20, 2024

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  multiversx_sdk/abi
  abi.py 335
  constants.py
  managed_decimal_signed_value.py 18-27, 33, 36-41, 45-47, 54-67, 74-76, 83, 86-88, 97
  managed_decimal_value.py 20, 45-47, 69, 74-76, 97
  multiversx_sdk/smart_contracts
  smart_contract_controller.py
Project Total  

This report was generated by python-coverage-comment-action

@@ -0,0 +1,41 @@
from decimal import Decimal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this affect us?

>>> from decimal import *
>>> getcontext().prec
28 

Maybe test the case with more decimals?

Copy link
Collaborator Author

@popenta popenta Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say 28 decimals is plenty given the fact that we don't compute anything using ManagedDecimal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking, most operations (formatting, scaleb etc.) are influenced by this setting (as far as I know).

A double / triple check (research) is worth around this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did some reading, added a local context with 256 decimals


assert isinstance(second_input, ManagedDecimalValue)
assert second_input.is_variable
assert second_input.scale == 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thus, scale == 0 means variable, correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed.

Comment on lines 325 to 332
if scale == "usize":
scale = 0
is_variable = True
else:
scale = int(scale)
is_variable = False

return ManagedDecimalValue(scale=scale, is_variable=is_variable)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be a bit more compact: if that return object, else return object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, done

from pathlib import Path

import pytest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PR description, please also reference the PRs that brought the implementation in JS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

def set_payload(self, value: Any):
if isinstance(value, ManagedDecimalSignedValue):
if self.is_variable != value.is_variable:
raise Exception("Cannot set payload! Both ManagedDecimalValues should be variable.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ManagedDecimalValues can be managed decimal values.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


if self.is_variable:
# read biguint value length in bytes
big_int_size = self._unsigned_from_bytes(data[:U32_SIZE_IN_BYTES])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is value_length a better name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed.

return value_str
if len(value_str) <= self.scale:
# If the value is smaller than the scale, prepend zeros
value_str = "0" * (self.scale - len(value_str) + 1) + value_str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it did. refactored the method

def _unsigned_from_bytes(self, data: bytes) -> int:
return int.from_bytes(data, byteorder="big", signed=False)

def _convert_value_to_int(self) -> int:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this help?

>>> from decimal import Decimal
>>> int(Decimal("123.456").scaleb(3))
123456

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it did. refactored this method, as well.

@@ -0,0 +1,103 @@
import io
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as for ManagedDecimalSignedValue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applied the same changes here, as well.

from multiversx_sdk.abi.managed_decimal_value import ManagedDecimalValue


class TestManagedDecimalValueTest:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe have a single test file to test both signed and unsigned values?

Also, maybe have some unit tests around encoding and decoding, as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now having the tests in the same file. did some encoding and decoding tests, as well.


def get_precision(self) -> int:
return len(str(self._convert_value_to_int()).lstrip("0"))
value_str = f"{self.value:.{self.scale}f}"
return len(value_str.replace(".", ""))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work always / when using other locales (i.e. USA, German etc.)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yes, because it's locale independent since we explicitly format the string using a dot and then replacing the dot.

value_str = "0" * (self.scale - len(value_str) + 1) + value_str
return f"{value_str[:-self.scale]}.{value_str[-self.scale:]}"
scaled_value = self._convert_value_to_int()
return f"{scaled_value / (10 ** self.scale):.{self.scale}f}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the assumption . is the decimals separator. Please double check if all right in all cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as decided, to_string() was removed.

Comment on lines 79 to 83
scaled_value = self._convert_value_to_int()
return f"{scaled_value / (10 ** self.scale):.{self.scale}f}"

def get_precision(self) -> int:
return len(str(self._convert_value_to_int()).lstrip("0"))
value_str = f"{self.value:.{self.scale}f}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A double check is necessary here, as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be alright, as well. It behaves like the JS implementation.

@@ -0,0 +1,41 @@
from decimal import Decimal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking, most operations (formatting, scaleb etc.) are influenced by this setting (as far as I know).

A double / triple check (research) is worth around this.

else:
return ManagedDecimalValue(scale=int(scale), is_variable=False)


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe follow the same pattern as for ManagedDecimal

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, done

@popenta popenta merged commit 1a59b79 into feat/next Jan 14, 2025
6 checks passed
@popenta popenta deleted the manged-decimal branch January 14, 2025 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants