clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,304 @@
# FRACTION
A Fraction class and field type for Drupal.
## CONTENTS OF THIS FILE
* Introduction
* Requirements
* Installation
* Usage
* Fraction Class
* Fraction Field
* Automatic Precision
* Database Storage
* Views Integration
* Using This Module for Price Storage
* Maintainers
## INTRODUCTION
This module provides two things:
1. A Fraction PHP class for representing and working with fractions.
2. A Fraction field with 2 widgets and 3 formatters.
For a full description of the module, visit the project page:
https://www.drupal.org/project/fraction
To submit bug reports and feature suggestions, or to track changes:
https://www.drupal.org/project/issues/fraction
## REQUIREMENTS
This module requires no modules outside of Drupal core.
## INSTALLATION
Install as you would normally install a contributed Drupal module. Visit:
https://www.drupal.org/docs/8/extending-drupal-8/installing-drupal-8-modules
for further information.
## FRACTION CLASS
Usage:
```
$fraction = new \Drupal\fraction\Fraction(1, 2);
```
Get the numerator and denominator (as strings):
```
$numerator = $fraction->getNumerator();
$denominator = $fraction->getDenominator();
```
Convert the fraction to a decimal with precision 2:
```
$precision = 2;
$decimal = $fraction->toDecimal($precision);
```
Multiply fractions:
```
$fraction1 = new \Drupal\fraction\Fraction(2, 3);
$fraction2 = new \Drupal\fraction\Fraction(1, 2);
$fraction1->multiply($fraction2);
```
Operations are performed using the BCMath PHP extension, when available.
Otherwise, normal PHP float operations are used.
## FRACTION FIELD
The Fraction field allows you to easily add fraction-based decimal storage to
your entities using Drupal's Field API.
### Widgets
(for editing the field)
**Fraction** - Two separate text fields for specifying the numerator and
denominator separately. This is the best way to store very specific fractions,
ie: 1/3.
**Decimal** - A single textfield that accepts a decimal representation of a
fraction (ie: 0.33333). This is useful when working with prices. Note that the
decimal is converted into a fraction with a base-10 denominator before saving
(ie: 0.33 becomes 33/100).
### Formatters
(for viewing the field)
**Fraction** - Simply displays the fraction's numerator and denominator
separated by a slash, ie: 1/3. The separator can be configured per-field.
**Decimal** - Displays the fraction as a decimal with a fixed precision. For
example, the fraction 1/3 can be represented with a precision of 5 as 0.33333.
**Percentage** - Displays the fraction as a percentage with a fixed precision.
This works the same as the **Decimal** formatter, but the value is multiplied
by 100. For example, the fraction 1/3 can be represented with a precision of
5 as 33.33333%.
## AUTOMATIC PRECISION
Fraction can automatically determine the precision of a fraction, as long as the
denominator is base 10.
Automatic precision can be used in the fraction class's toDecimal() method by
setting the second parameter to TRUE:
```
$precision = 2;
$auto_precision = TRUE;
$decimal = $fraction->toDecimal($precision, TRUE);
```
In the above example, if the fraction's denominator is not base 10, then the
precision defined in the first parameter will be used. If the denominator is
base 10, the precision will be automatically calculated.
The "Decimal" widget and formatter both provide an option to enable automatic
precision.
## DATABASE STORAGE
Fractions are stored in the database using two columns: a numerator and a
denominator.
The numerator is represented as a signed BIGINT, which allows for a range of
values between -9223372036854775808 and 9223372036854775807.
The denominator is represented as a signed INT, which allows for a range of
values between -2147483648 to +2147483647, but a constraint is added to ensure
it is always a positive non-zero integer.
For modules that want to implement their own numerator and denominator columns
in a database table, the following schema can be used as an example (for use in
hook_schema()):
```
/**
* Implements hook_schema().
*/
function mymodule_schema() {
$schema['mymodule_table'] = [
'fields' => [
...
'value_numerator' => [
'description' => 'Value numerator',
'type' => 'int',
'size' => 'big',
'not null' => FALSE,
],
'value_denominator' => [
'description' => 'Value denominator',
'type' => 'int',
'not null' => FALSE,
],
...
],
];
return $schema;
}
```
## VIEWS INTEGRATION
Views integration is provided by Views itself on behalf of the core Field
module. Fraction extends some of the field handlers to allow sorting and
filtering by the decimal equivalent of the fraction value (calculated via
formula in the database query).
For modules that want to implement their own fraction-based database storage
(see Database Storage above), Fraction also provides a general-purpose Views
field handler that can be used in hook_views_data(). Using the same example
described in Database Storage above, here is what that would look like:
```
/**
* Implements hook_views_data().
*/
function mymodule_views_data() {
...
// Value numerator.
$data['mymodule_table']['value_numerator'] = [
'title' => t('Value numerator'),
'help' => t('The stored numerator value.'),
'field' => [
'id' => 'numeric',
'click sortable' => TRUE,
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
];
// Value denominator.
$data['mymodule_table']['value_denominator'] = [
'title' => t('Value denominator'),
'help' => t('The stored denominator value.'),
'field' => [
'id' => 'numeric',
'click sortable' => TRUE,
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
];
// Create a new decimal column with fraction decimal handlers.
$fraction_fields = [
'numerator' => 'value_numerator',
'denominator' => 'value_denominator',
];
$data['mymodule_table']['value_decimal'] = [
'title' => t('Value (decimal)'),
'help' => t('Decimal equivalent of the value.'),
'real field' => 'value_numerator',
'field' => [
'id' => 'fraction',
'additional fields' => $fraction_fields,
'click sortable' => TRUE,
],
'sort' => [
'id' => 'fraction',
'additional fields' => $fraction_fields,
],
'filter' => [
'id' => 'fraction',
'additional fields' => $fraction_fields,
]
];
return $data;
}
```
## USING THIS MODULE FOR PRICE STORAGE
This module aims to solve an outstanding issue with price storage in Drupal.
Storing prices in the database can be tricky if you don't know what the decimal
precision will need to be from the beginning. This is often the case when
multiple currencies need to be supported, or when you need extra flexibility
with decimal precision.
The 7.x-1.x branch of [Drupal Commerce](http://drupal.org/project/commerce),
for instance, ties precision directly to currency. So if all of your products
are sold with a two-decimal price, but you want to add one with three decimal
places, you need to add a second (duplicate) currency for just those products.
There is an outstanding issue in the Commerce queue that is discussing solutions
(one of which is to use a Fraction-based approach):
https://www.drupal.org/node/1125706
The alternative to using a decimal-based storage is to use floats. This brings
more issues with precision, due to the way that floating-point numbers are
represented in lower-level system storage. The limitations of float precision
are outlined on PHP's website:
http://www.php.net/manual/en/language.types.float.php (If you're REALLY
interested, here's a great video that explains floating-point rounding errors:
https://www.youtube.com/watch?v=PZRI1IfStY0)
Fraction overcomes these issues by storing the value as a fraction, using two
integer fields in the database. Decimal representation of this fraction is
provided in higher-level functions.
The best way to use this module for price storage is to utilize the "Decimal
field" widget to collect the price, and the "Decimal" formatter to display it.
This approach hides the fact that there are actually two numbers being stored in
the database: numerator and denominator. This is achieved through a simple
decimal-to-fraction conversion when a decimal value is saved.
When a decimal is converted to a fraction, the numerator and denominator are
calculated such that the denominator is a power of 10. For example, the decimal
13.95 becomes 1395/100 in the database.
This means that for price storage, this module supports up to 9 decimal places
of precision (because the maximum denominator is 4,294,967,295, meaning the
highest possible base-10 number that fits into that is 1,000,000,000).
Ultimately, the maximum numerator depends on the size of the denominator. But,
using the maximum precision (of 9 decimal places), the biggest number this can
store is 9,223,372,036.854775807.
## MAINTAINERS
Current maintainers:
* Michael Stenta (m.stenta) - https://drupal.org/user/581414
* Pedro Cambra (pcambra) - https://drupal.org/user/122101

View File

@@ -0,0 +1,17 @@
{
"name": "drupal/fraction",
"type": "drupal-module",
"description": "Provides a field for storing decimals as fractions, for maximum precision.",
"keywords": ["Drupal"],
"license": "GPL-2.0-or-later",
"homepage": "https://www.drupal.org/project/fraction",
"minimum-stability": "dev",
"support": {
"issues": "http://drupal.org/project/issues/fraction",
"source": "http://cgit.drupalcode.org/fraction"
},
"require": { },
"require-dev": {
"drupal/feeds": "^3.0"
}
}

View File

@@ -0,0 +1,120 @@
field.field_settings.fraction:
type: mapping
label: 'Fraction'
mapping:
prefix:
type: label
label: 'Prefix'
suffix:
type: label
label: 'Suffix'
min:
type: float
label: 'Min'
max:
type: float
label: 'Max'
field.formatter.settings.fraction:
type: mapping
label: 'Fraction display format settings'
mapping:
separator:
type: string
label: 'Separator'
prefix_suffix:
type: boolean
label: 'Display prefix and suffix.'
field.formatter.settings.fraction_decimal:
type: mapping
label: 'Fraction decimal display format settings'
mapping:
precision:
type: integer
label: 'Precision'
auto_precision:
type: boolean
label: 'Auto precision'
prefix_suffix:
type: boolean
label: 'Display prefix and suffix.'
field.formatter.settings.fraction_percentage:
type: mapping
label: 'Fraction percentage display format settings'
mapping:
precision:
type: integer
label: 'Precision'
auto_precision:
type: boolean
label: 'Auto precision'
prefix_suffix:
type: boolean
label: 'Display prefix and suffix.'
field.widget.settings.fraction_decimal:
type: mapping
label: 'Widget settings for Fraction Decimal'
mapping:
precision:
type: integer
label: 'Precision'
auto_precision:
type: boolean
label: 'Auto precision'
field.value.fraction:
type: mapping
label: 'Default value'
mapping:
numerator:
type: integer
label: 'Numerator'
denominator:
type: integer
label: 'Denominator'
views.field.fraction:
type: views_field
label: 'Fraction'
mapping:
precision:
type: integer
label: 'Precision'
auto_precision:
type: boolean
label: 'Auto precision'
views.filter.fraction:
type: views_filter
label: 'Numeric'
mapping:
expose:
type: mapping
label: 'Exposed'
mapping:
min_placeholder:
type: label
label: 'Min placeholder'
max_placeholder:
type: label
label: 'Max placeholder'
placeholder:
type: label
label: 'Placeholder'
views.filter_value.fraction:
type: mapping
label: 'Fraction'
mapping:
min:
type: string
label: 'Min'
max:
type: string
label: 'And max'
value:
type: string
label: 'Value'

View File

@@ -0,0 +1,15 @@
name: Fraction
type: module
description: Provides a field for storing decimals as fractions, for maximum precision.
core_version_requirement: ^9.1 || ^10
package: Field types
dependencies:
- drupal:field
test_dependencies:
- feeds:feeds
# Information added by Drupal.org packaging script on 2023-08-21
version: '2.3.1'
project: 'fraction'
datestamp: 1692651922

View File

@@ -0,0 +1,24 @@
<?php
/**
* @file
* Install, update, and uninstall functions for the fraction module.
*/
/**
* Implements hook_requirements().
*/
function fraction_requirements($phase) {
$requirements = [];
// If BCMath is not available, add a note to the status page.
if ($phase == 'runtime' && !function_exists('bcadd')) {
$requirements['fraction'] = [
'title' => t('BCMath is not available'),
'description' => t('The Fraction module uses BCMath (when available) to ensure calculations are performed with maximum precision. Without BCMath, Fraction will still work, but normal PHP float arithmetic will be used. For an overview of float arithmetic limitations, see <a href="http://php.net/manual/en/language.types.float.php">http://php.net/manual/en/language.types.float.php</a>.'),
'severity' => REQUIREMENT_WARNING,
];
}
return $requirements;
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* @file
* Fraction module code.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function fraction_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the fraction module.
case 'help.page.fraction':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Provides a field for storing decimals as fractions, for maximum precision.') . '</p>';
return $output;
default:
}
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* @file
* Post update functions for Fraction.
*/
use Drupal\Core\Utility\UpdateException;
/**
* Alter schema to make denominator signed.
*
* This is done so the field is leveled between MySQL and Postgres and migration
* is possible between the two. When a integer is unsigned in Postges, Drupal
* will create a bigint for it, while with MySQL, it would create a regular int.
*
* @see \Drupal\Core\Database\Driver\pgsql\Schema::processField()
* @see https://www.drupal.org/project/fraction/issues/2729315
*/
function fraction_post_update_make_denominator_signed() {
$entity_type_manager = \Drupal::entityTypeManager();
$entity_field_manager = \Drupal::service('entity_field.manager');
$entity_storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
$entity_field_manager->clearCachedFieldDefinitions();
$updated_fields = $fields_to_update = [];
// Get all fraction fields in the system.
$fraction_fields = $entity_field_manager->getFieldMapByFieldType('fraction');
foreach ($fraction_fields as $entity_type_id => $fields) {
$table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
$field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
foreach ($fields as $field_name => $field_info) {
$storage_definition = $entity_field_manager->getFieldStorageDefinitions($entity_type_id)[$field_name];
$column_name = $table_mapping->getFieldColumnName($storage_definition, 'denominator');
$table_name = $table_mapping->getFieldTableName($field_name);
$max_denominator = \Drupal::database()->query('SELECT MAX(' . $column_name . ') FROM {' . $table_name . '}')->fetchField();
$revision_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]);
if (\Drupal::database()->schema()->tableExists($revision_table_name)) {
$max_revision_denominator = \Drupal::database()->query('SELECT MAX(' . $column_name . ') FROM {' . $revision_table_name . '}')->fetchField();
$max_denominator = max($max_denominator, $max_revision_denominator);
}
// Max value on signed int for MySQL or int in PostgreSQL is 2147483648.
if ($max_denominator >= 2147483648) {
// If the max value is bigger than the limits for MySQL/Postgres, warn
// the user.
throw new UpdateException('Fraction works with signed integer schema fields for denominator, some of your values in the database exceed this limit, please check the fraction field tables and review the data before running this update. See https://www.drupal.org/project/fraction/issues/2729315 for further details.');
}
else {
\Drupal::service('entity.last_installed_schema.repository')->setLastInstalledFieldStorageDefinition($storage_definition);
$field_schema = $entity_storage_schema->get("$entity_type_id.field_schema_data.$field_name");
$fields_to_update[] = compact('field_name', 'table_name', 'column_name', 'entity_type_id', 'field_schema', 'revision_table_name');
}
}
}
if (empty($fields_to_update)) {
return 'No fraction fields found to update.';
}
foreach ($fields_to_update as $field_to_update) {
$field_name = $field_to_update['field_name'];
$table_name = $field_to_update['table_name'];
$column_name = $field_to_update['column_name'];
$entity_type_id = $field_to_update['entity_type_id'];
$field_schema = $field_to_update['field_schema'];
$revision_table_name = $field_to_update['revision_table_name'];
fraction_alter_denominator_helper($table_name, $column_name);
unset($field_schema[$table_name]['fields'][$column_name]['unsigned']);
if (\Drupal::database()->schema()->tableExists($revision_table_name)) {
unset($field_schema[$revision_table_name]['fields'][$column_name]['unsigned']);
}
// Ensure that the field schema is updated accordingly to the change of
// the field so the entity updates service does not alert of this
// change.
$entity_storage_schema->set("$entity_type_id.field_schema_data.$field_name", $field_schema);
$updated_fields[] = $field_name;
}
$entity_field_manager->clearCachedFieldDefinitions();
return 'Fraction fields updated: ' . implode(',', $updated_fields);
}
/**
* Helper function that updates the field schema for denominator.
*
* @param string $table_name
* The database table name.
* @param string $column_name
* The database column name.
*/
function fraction_alter_denominator_helper($table_name, $column_name) {
\Drupal::database()->schema()->changeField($table_name, $column_name, $column_name, [
'description' => 'Fraction denominator value',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
]);
}
/**
* Alter schema for the not null changes.
*
* @see https://www.drupal.org/project/fraction/issues/3223868
*/
function fraction_post_update_not_null_field_schema_fix() {
$entity_type_manager = \Drupal::entityTypeManager();
$entity_field_manager = \Drupal::service('entity_field.manager');
$entity_storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
$last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
$database_schema = \Drupal::database()->schema();
$updated_fields = $fields_to_update = [];
$entity_field_manager->clearCachedFieldDefinitions();
// Get all fraction fields in the system.
$fraction_fields = $entity_field_manager->getFieldMapByFieldType('fraction');
foreach ($fraction_fields as $entity_type_id => $fields) {
$table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
$field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
foreach ($fields as $field_name => $field_info) {
$storage_definition = $field_storage_definitions[$field_name];
$column_names = [
$table_mapping->getFieldColumnName($storage_definition, 'numerator'),
$table_mapping->getFieldColumnName($storage_definition, 'denominator'),
];
$table_name = $table_mapping->getFieldTableName($field_name);
$revision_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]);
$last_installed_schema_repository->setLastInstalledFieldStorageDefinition($storage_definition);
$field_schema = $entity_storage_schema->get("$entity_type_id.field_schema_data.$field_name");
$fields_to_update[] = compact('field_name', 'table_name', 'column_names', 'entity_type_id', 'field_schema', 'revision_table_name');
}
}
if (empty($fields_to_update)) {
return 'No fraction fields found to update.';
}
foreach ($fields_to_update as $field_to_update) {
$field_name = $field_to_update['field_name'];
$table_name = $field_to_update['table_name'];
$column_names = $field_to_update['column_names'];
$entity_type_id = $field_to_update['entity_type_id'];
$field_schema = $field_to_update['field_schema'];
$revision_table_name = $field_to_update['revision_table_name'];
foreach ($column_names as $column_name) {
$database_schema->changeField($table_name, $column_name, $column_name, [
'description' => 'Fraction ' . $column_name . ' value',
'type' => 'int',
'not null' => FALSE,
]);
unset($field_schema[$table_name]['fields'][$column_name]['default']);
if ($database_schema->tableExists($revision_table_name)) {
unset($field_schema[$revision_table_name]['fields'][$column_name]['default']);
}
}
// Ensure that the field schema is updated accordingly to the change of
// the field so the entity updates service does not alert of this
// change.
$entity_storage_schema->set("$entity_type_id.field_schema_data.$field_name", $field_schema);
$updated_fields[] = $field_name;
}
$entity_field_manager->clearCachedFieldDefinitions();
return 'Fraction fields updated: ' . implode(',', $updated_fields);
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @file
* Views hooks.
*/
/**
* Implements hook_field_views_data_alter().
*/
function fraction_field_views_data_alter(&$data, $field) {
// Only operate on fields defined by the Fraction module.
if ($field->getTypeProvider() != 'fraction') {
return;
}
// Get the field name.
$field_name = $field->getName();
// Get the entity type that this field is attached to.
$entity_type = $field->getTargetEntityTypeId();
// Look up the field's most used instance label.
$field_label = views_entity_field_label($entity_type, $field_name)[0];
// Build a list of additional fields for the sort and filter handlers.
$additional_fields = [
'numerator' => $field_name . '_numerator',
'denominator' => $field_name . '_denominator',
];
// Iterate through the results.
foreach ($data as &$definition) {
// We will want to alter Views data for both the current field definition,
// and the field revision definition. The current field definition will be
// in $definition[$field_name], while the field revision definiton will be
// in $definition[$field_name . '-revision_id']. So we need to test for the
// presence of both and act accordingly. If neither exists, stop here and
// skip to the next definition.
// @see views_field_default_views_data()
$field_revision_name = $field_name . '-revision_id';
if (empty($definition[$field_name]) && empty($definition[$field_revision_name])) {
continue;
}
elseif (!empty($definition[$field_name . '-revision_id'])) {
$field_name = $field_revision_name;
}
// Override the field handler so that we can provide our own custom
// click sort method (that uses the fraction's decimal equivalent).
$definition[$field_name]['field']['id'] = 'fraction_field';
// Make the field click-sortable.
$definition[$field_name]['field']['click sortable'] = TRUE;
// Create a new decimal column with custom sort and filter handlers.
$column_name = $field_name . '_decimal';
$definition[$column_name] = [
'group' => $definition[$field_name]['group']->getUntranslatedString(),
'title' => t('@field_label (decimal)', [
'@field_label' => $field_label,
]),
'title short' => t('@field_label:decimal', [
'@field_label' => $field_label,
]),
'help' => t('Decimal equivalent of Fraction field for sorting and filtering. @extra', [
'@extra' => $definition[$field_name]['help'],
]),
'sort' => [
'id' => 'fraction',
'additional fields' => $additional_fields,
],
'filter' => [
'id' => 'fraction',
'additional fields' => $additional_fields,
],
];
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Drupal\fraction\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
/**
* Provides a fraction form element.
*
* Usage example:
* @code
* $form['fraction'] = [
* '#type' => 'fraction',
* '#title' => $this->t('Fraction'),
* '#default_value' => ['numerator' => 10, 'denominator' => 100],
* '#size' => 60,
* '#required' => TRUE,
* ];
* @endcode
*
* @FormElement("fraction")
*/
class Fraction extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#size' => 10,
'#element_validate' => [
[$class, 'validateFraction'],
],
'#process' => [
[$class, 'processElement'],
[$class, 'processAjaxForm'],
[$class, 'processGroup'],
],
'#pre_render' => [
[$class, 'preRenderGroup'],
],
'#theme_wrappers' => ['container'],
];
}
/**
* Builds the fraction form element.
*
* @param array $element
* The initial fraction form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The built fraction form element.
*/
public static function processElement(array $element, FormStateInterface $form_state, array &$complete_form) {
$default_value = $element['#default_value'];
if (isset($default_value) && !self::validateDefaultValue($default_value)) {
throw new \InvalidArgumentException('The #default_value for a fraction element must be an array with "numerator" and "denominator" keys.');
}
$element['#tree'] = TRUE;
$element['#attributes']['class'][] = 'form-type-fraction';
$element['numerator'] = [
'#type' => 'number',
'#title' => t('Numerator'),
'#title_display' => $element['#title_display'],
'#default_value' => $default_value ? $default_value['numerator'] : NULL,
'#required' => $element['#required'],
'#size' => $element['#size'],
'#error_no_message' => TRUE,
];
$element['denominator'] = [
'#type' => 'number',
'#title' => t('Denominator'),
'#title_display' => $element['#title_display'],
'#default_value' => $default_value ? $default_value['denominator'] : NULL,
'#description' => !empty($element['#description']) ?? '',
'#required' => $element['#required'],
'#size' => $element['#size'],
'#error_no_message' => TRUE,
];
// Remove the keys that were transferred to child elements.
unset($element['#size']);
unset($element['#description']);
return $element;
}
/**
* Form element validation handler for #type 'fraction'.
*
* Note that #required is validated by _form_validate() already.
*/
public static function validateFraction(&$element, FormStateInterface $form_state, &$complete_form) {
$numerator = $element['numerator']['#value'];
$denominator = $element['denominator']['#value'];
// If the denominator is empty, but the numerator isn't, print an error.
if (empty($denominator) && !empty($numerator)) {
$form_state->setError($element, t('The denominator of a fraction cannot be zero or empty (if a numerator is provided).'));
}
// Numerators must be between -9223372036854775808 and 9223372036854775807.
// Explicitly perform a string comparison to ensure precision.
if (!empty($numerator) && ((string) $numerator < '-9223372036854775808' || (string) $numerator > '9223372036854775807')) {
$form_state->setError($element, t('The numerator of a fraction must be between -9223372036854775808 and 9223372036854775807.'));
}
// Denominators must be between 1 and 2147483647.
// Explicitly perform a string comparison to ensure precision.
if (!empty($denominator) && ((string) $denominator <= '0' || (string) $denominator > '2147483647')) {
$form_state->setError($element, t('The denominator of a fraction must be greater than 0 and less than 2147483647.'));
}
}
/**
* Validates the default value.
*
* @param mixed $default_value
* The default value.
*
* @return bool
* TRUE if the default value is valid, FALSE otherwise.
*/
public static function validateDefaultValue($default_value) {
if (!is_array($default_value)) {
return FALSE;
}
if (!array_key_exists('numerator', $default_value) || !array_key_exists('denominator', $default_value)) {
return FALSE;
}
return TRUE;
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Drupal\fraction\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Render\Element\Number;
use Drupal\fraction\Fraction;
/**
* Provides a fraction decimal form element.
*
* Usage example:
* @code
* $form['fraction'] = [
* '#type' => 'fraction_decimal',
* '#title' => $this->t('Fraction'),
* '#default_value' => 10.385,
* '#size' => 60,
* '#required' => TRUE,
* ];
* @endcode
*
* @FormElement("fraction_decimal")
*/
class FractionDecimal extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#step' => 'any',
'#size' => 15,
'#maxlength' => 128,
'#default_value' => NULL,
'#element_validate' => [
[$class, 'validateDecimal'],
],
'#process' => [
[$class, 'processAjaxForm'],
[$class, 'processGroup'],
],
'#pre_render' => [
[$class, 'preRenderNumber'],
[$class, 'preRenderGroup'],
],
'#theme' => 'input__textfield',
'#theme_wrappers' => ['form_element'],
];
}
/**
* Validates the fraction_decimal element.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*/
public static function validateDecimal(array $element, FormStateInterface $form_state, array &$complete_form) {
$value = trim($element['#value']);
// Only continue with validation if the value is not empty.
if ($element['#value'] === '') {
return;
}
// Do basic number validations.
Number::validateNumber($element, $form_state, $complete_form);
// Only continue with validation if value is a valid number.
if (count($form_state->getErrors()) !== 0) {
return;
}
// Convert the value to a fraction.
$fraction = Fraction::createFromDecimal($value);
// Get the numerator and denominator.
$numerator = $fraction->getNumerator();
$denominator = $fraction->getDenominator();
// Set the numerator and denominator values for the form.
$values = [
'decimal' => $value,
'numerator' => $numerator,
'denominator' => $denominator,
];
$form_state->setValueForElement($element, $values);
// The maximum number of digits after the decimal place is 9.
// Explicitly perform a string comparison to ensure precision.
if ((string) $denominator > '1000000000') {
$form_state->setError($element, t('The maximum number of digits after the decimal place is 9.'));
}
// Ensure that the decimal value is within an acceptable value range.
// Convert the fraction back to a decimal, because that is what will be
// stored. Explicitly perform a string comparison to ensure precision.
$decimal = (string) $fraction->toDecimal(0, TRUE);
$min_decimal_fraction = new Fraction('-9223372036854775808', $denominator);
$min_decimal = (string) $min_decimal_fraction->toDecimal(0, TRUE);
$max_decimal_fraction = new Fraction('9223372036854775807', $denominator);
$max_decimal = (string) $max_decimal_fraction->toDecimal(0, TRUE);
$scale = strlen($denominator) - 1;
$in_bounds = static::checkInBounds($decimal, $min_decimal, $max_decimal, $scale);
if (!$in_bounds) {
$form_state->setError($element, t('The number you entered is outside the range of acceptable values. This limitation is related to the decimal precision, so reducing the precision may solve the problem.'));
}
}
/**
* Prepares a #type 'fraction_decimal' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #description, #size, #maxlength,
* #placeholder, #required, #attributes.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderNumber(array $element) {
$element['#attributes']['type'] = 'text';
Element::setAttributes($element,
['id', 'name', 'value', 'size', 'maxlength', 'placeholder']
);
static::setAttributes($element, ['form-text']);
return $element;
}
/**
* Helper method to check if a given value is in between two other values.
*
* Uses BCMath and strings for arbitrary-precision operations where possible.
*
* @param string $value
* The value to check.
* @param string $min
* The minimum bound.
* @param string $max
* The maximum bound.
* @param int $scale
* Optional scale integer to pass into bcsub() if BCMath is used.
*
* @return bool
* Returns TRUE if $number is between $min and $max, FALSE otherwise.
*/
public static function checkInBounds($value, $min, $max, $scale = 0) {
// If BCMath isn't available, let PHP handle it via normal float comparison.
if (!function_exists('bcsub')) {
return ($value > $max || $value < $min) ? FALSE : TRUE;
}
// Subtract the minimum bound and maximum bounds from the value.
$diff_min = bcsub($value, $min, $scale);
$diff_max = bcsub($value, $max, $scale);
// If either have a difference of zero, then the value is in bounds.
if ($diff_min == 0 || $diff_max == 0) {
return TRUE;
}
// If the first character of $diff_min is a negative sign (-), then the
// value is less than the minimum, and therefore out of bounds.
if (substr($diff_min, 0, 1) == '-') {
return FALSE;
}
// If the first character of $diff_max is a number, then the value is
// greater than the maximum, and therefore out of bounds.
if (is_numeric(substr($diff_max, 0, 1))) {
return FALSE;
}
// Assume the value is in bounds if none of the above said otherwise.
return TRUE;
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Drupal\fraction\Feeds\Target;
use Drupal\Core\Form\FormStateInterface;
use Drupal\feeds\Plugin\Type\Target\ConfigurableTargetInterface;
use Drupal\feeds\Plugin\Type\Target\FieldTargetBase;
use Drupal\fraction\Fraction;
/**
* Defines a fraction field mapper.
*
* @FeedsTarget(
* id = "fraction",
* field_types = {
* "fraction",
* }
* )
*/
class FractionTarget extends FieldTargetBase implements ConfigurableTargetInterface {
/**
* Helper function to define the ways to import the fraction data.
*
* @return array
* Array with all the possible types.
*/
protected function importTypes() {
return [
'fraction' => $this->t('Fraction'),
'decimal' => $this->t('Decimal'),
];
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['type' => 'fraction'];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['type'] = [
'#type' => 'select',
'#title' => $this->t('Filter format'),
'#options' => $this->importTypes(),
'#default_value' => $this->configuration['type'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function getSummary() {
$types = $this->importTypes();
return $this->t('Import type: %type', ['%type' => $types[$this->configuration['type']]]);
}
/**
* {@inheritdoc}
*/
protected function prepareValue($delta, array &$values) {
$item = is_string($values['value']) ? trim($values['value']) : $values['value'];
unset($values['value']);
switch ($this->configuration['type']) {
case 'fraction':
// Pull out the numerator and denominator.
$parts = is_string($item) ? explode('/', $item) : [];
if (!empty($parts[0]) && is_numeric($parts[0]) && !empty($parts[1]) && is_numeric($parts[1]) && $parts[1] >= 0) {
$values['numerator'] = $parts[0];
$values['denominator'] = $parts[1];
}
else {
$values['numerator'] = '';
$values['denominator'] = '';
}
break;
case 'decimal':
// If item is non-numeric or an empty string, create an empty fraction.
// Otherwise, pass the value to createFromDecimal().
if (!is_numeric($item) || $item === '') {
$values['numerator'] = '';
$values['denominator'] = '';
}
else {
$fraction = Fraction::createFromDecimal($item);
$values['numerator'] = $fraction->getNumerator();
$values['denominator'] = $fraction->getDenominator();
}
break;
}
}
}

View File

@@ -0,0 +1,428 @@
<?php
namespace Drupal\fraction;
/**
* A simple class for representing and acting upon a fraction.
*/
class Fraction implements FractionInterface {
/**
* Numerator of the fraction.
*
* @var string
*/
protected $numerator;
/**
* Denominator of the fraction.
*
* @var string
*/
protected $denominator;
/**
* Constructs a Fraction object.
*
* @param string|int $numerator
* The fraction's numerator. Defaults to 0.
* @param string|int $denominator
* The fraction's denominator. Defaults to 1.
*/
public function __construct($numerator = 0, $denominator = 1) {
$this->setNumerator($numerator);
$this->setDenominator($denominator);
}
/**
* {@inheritdoc}
*/
public static function createFromDecimal($value) {
// Calculate the precision by counting the number of decimal places.
$precision = strlen(substr(strrchr($value, '.'), 1));
// Create the denominator by raising 10 to the power of the precision.
if (function_exists('bcpow')) {
$denominator = bcpow(10, $precision);
}
else {
$denominator = pow(10, $precision);
}
// Calculate the numerator by multiplying the value by the denominator.
if (function_exists('bcmul')) {
$numerator = bcmul($value, $denominator, 0);
}
else {
$numerator = $value * $denominator;
}
return new Fraction($numerator, $denominator);
}
/**
* {@inheritdoc}
*/
public function setNumerator($value) {
// Cast the value as a string and save it.
$this->numerator = (string) $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function setDenominator($value) {
// Protect against division by zero.
if (empty($value)) {
$this->setNumerator(0);
$value = 1;
}
// Normalize negative fractions.
// If the denominator is negative, invert the signs for both numbers.
if ($value < 0) {
$numerator = $this->getNumerator();
$numerator = $numerator * -1;
$this->setNumerator($numerator);
$value = $value * -1;
}
// Cast the value as a string and save it.
$this->denominator = (string) $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function getNumerator() {
return $this->numerator;
}
/**
* {@inheritdoc}
*/
public function getDenominator() {
return $this->denominator;
}
/**
* {@inheritdoc}
*/
public function toString(string $separator = '/') {
// Get the numerator and denominator.
$numerator = $this->getNumerator();
$denominator = $this->getDenominator();
// Concatenate with the separator and return.
return $numerator . $separator . $denominator;
}
/**
* {@inheritdoc}
*/
public function toDecimal(int $precision = 0, bool $auto_precision = FALSE) {
// Get the numerator and denominator.
$numerator = $this->getNumerator();
$denominator = $this->getDenominator();
// If auto precision is on figure out the maximum precision.
if ($auto_precision) {
// If the denominator is base-10, max precision is the number of zeroes
// in the denominator.
if ($denominator % 10 == 0) {
$max_precision = strlen($denominator) - 1;
}
// Or, if the denominator is 1, max precision is zero.
elseif ($denominator == 1) {
$max_precision = 0;
}
// Or, if the denominator is a multiple of 2 or 5, the fraction is known
// to be terminating, and we can figure out the precision.
elseif ($this->isTerminating()) {
$max_precision = $this->terminatingPrecision($this->reduce()->getDenominator());
}
// Otherwise, max precision is the denominator length.
else {
$max_precision = strlen($denominator);
}
// Use the greater of the two precisions.
$precision = ($max_precision > $precision) ? $max_precision : $precision;
}
// Divide the numerator by the denominator (using BCMath if available).
if (function_exists('bcdiv')) {
// Divide the numerator and denominator, with extra precision.
$value = bcdiv($numerator, $denominator, $precision + 1);
// Return a decimal string rounded to the final precision.
return $this->bcRound($value, $precision);
}
// If BCMath is not available, use normal PHP float division and rounding.
else {
return (string) round($numerator / $denominator, $precision);
}
}
/**
* {@inheritdoc}
*/
public function gcd() {
// Get the numerator and denominator.
$numerator = $this->getNumerator();
$denominator = $this->getDenominator();
// Make sure both numbers are positive.
$a = str_replace('-', '', $numerator);
$b = str_replace('-', '', $denominator);
// Euclid's algorithm gives us the greatest common divisor.
// Use BCMath's modulus function if available.
if (function_exists('bcmod')) {
while ($b != 0) {
$t = $b;
$b = bcmod($a, $b);
$a = $t;
}
}
else {
while ($b != 0) {
$t = $b;
$b = $a % $b;
$a = $t;
}
}
return (string) $a;
}
/**
* {@inheritdoc}
*/
public function reduce() {
// Get the numerator and denominator.
$numerator = $this->getNumerator();
$denominator = $this->getDenominator();
// Calculate the greatest common divisor.
$gcd = $this->gcd();
// Divide the numerator and denominator by the gcd.
// Use BCMath division if available.
if (function_exists('bcdiv')) {
$numerator = bcdiv($numerator, $gcd, 0);
$denominator = bcdiv($denominator, $gcd, 0);
}
else {
$numerator = $numerator / $gcd;
$denominator = $denominator / $gcd;
}
// Create a new Fraction object.
return new Fraction($numerator, $denominator);
}
/**
* {@inheritdoc}
*/
public function reciprocate() {
// Get the numerator and denominator, but flipped.
$numerator = $this->getDenominator();
$denominator = $this->getNumerator();
// Create a new Fraction object.
return new Fraction($numerator, $denominator);
}
/**
* {@inheritdoc}
*/
public function add(Fraction $fraction) {
// Get the numerator and denominator of each fraction.
$numerator1 = $this->getNumerator();
$denominator1 = $this->getDenominator();
$numerator2 = $fraction->getNumerator();
$denominator2 = $fraction->getDenominator();
// Calculate the sum of the two fractions.
// Use BCMath if available.
if (function_exists('bcmul') && function_exists('bcadd')) {
$denominator = bcmul($denominator1, $denominator2, 0);
$numerator = bcadd(bcmul($numerator1, $denominator2, 0), bcmul($numerator2, $denominator1, 0), 0);
}
else {
$denominator = $denominator1 * $denominator2;
$numerator = $numerator1 * $denominator2 + $numerator2 * $denominator1;
}
// Create a new Fraction object and reduce it.
$fraction = new Fraction($numerator, $denominator);
$fraction = $fraction->reduce();
return $fraction;
}
/**
* {@inheritdoc}
*/
public function subtract(Fraction $fraction) {
// Get the numerator and denominator of each fraction.
$numerator1 = $this->getNumerator();
$denominator1 = $this->getDenominator();
$numerator2 = $fraction->getNumerator();
$denominator2 = $fraction->getDenominator();
// Calculate the difference of the two fractions.
// Use BCMath if available.
if (function_exists('bcmul') && function_exists('bcsub')) {
$denominator = bcmul($denominator1, $denominator2, 0);
$numerator = bcsub(bcmul($numerator1, $denominator2, 0), bcmul($numerator2, $denominator1, 0), 0);
}
else {
$denominator = $denominator1 * $denominator2;
$numerator = $numerator1 * $denominator2 - $numerator2 * $denominator1;
}
// Create a new Fraction object and reduce it.
$fraction = new Fraction($numerator, $denominator);
$fraction = $fraction->reduce();
return $fraction;
}
/**
* {@inheritdoc}
*/
public function multiply(Fraction $fraction) {
// Get the numerator and denominator of each fraction.
$numerator1 = $this->getNumerator();
$denominator1 = $this->getDenominator();
$numerator2 = $fraction->getNumerator();
$denominator2 = $fraction->getDenominator();
// Calculate the product of the two fractions.
// Use BCMath if available.
if (function_exists('bcmul')) {
$numerator = bcmul($numerator1, $numerator2, 0);
$denominator = bcmul($denominator1, $denominator2, 0);
}
else {
$numerator = $numerator1 * $numerator2;
$denominator = $denominator1 * $denominator2;
}
// Create a new Fraction object and reduce it.
$fraction = new Fraction($numerator, $denominator);
$fraction = $fraction->reduce();
return $fraction;
}
/**
* {@inheritdoc}
*/
public function divide(Fraction $fraction) {
// Reciprocate the fraction.
$reciprocal = $fraction->reciprocate();
// Get the numerator and denominator of each fraction.
$numerator1 = $this->getNumerator();
$denominator1 = $this->getDenominator();
$numerator2 = $reciprocal->getNumerator();
$denominator2 = $reciprocal->getDenominator();
// Calculate the quotient of the two fractions.
// Use BCMath if available.
if (function_exists('bcmul')) {
$numerator = bcmul($numerator1, $numerator2, 0);
$denominator = bcmul($denominator1, $denominator2, 0);
}
else {
$numerator = $numerator1 * $numerator2;
$denominator = $denominator1 * $denominator2;
}
// Create a new Fraction object and reduce it.
$fraction = new Fraction($numerator, $denominator);
$fraction = $fraction->reduce();
return $fraction;
}
/**
* Test if the fraction is terminating.
*
* @return bool
* Returns TRUE if the fraction is terminating, FALSE if it is repeating.
*/
protected function isTerminating() {
$d = $this->getDenominator();
$d /= $this->gcd();
while ($d % 2 == 0) {
$d /= 2;
}
while ($d % 5 == 0) {
$d /= 5;
}
return $d == 1;
}
/**
* Given the denominator of a terminating fraction, calculate the precision.
*
* @param string $denominator
* The denominator.
* @param int $exponent
* An exponent that is incremented with each iteration.
*
* @return int
* The number of decimal places required to represent the fraction as a
* decimal.
*/
protected function terminatingPrecision(string $denominator, int $exponent = 1) {
if (function_exists('bcpow')) {
$done = bcmod(bcpow(10, $exponent), $denominator) === '0';
}
else {
$done = pow(10, $exponent) % $denominator === 0;
}
return $done ? $exponent : $this->terminatingPrecision($denominator, $exponent + 1);
}
/**
* BCMath decimal rounding function.
*
* @param int|float|string $value
* The value to round.
* @param int $precision
* The desired decimal precision.
*
* @return string
* Returns a rounded decimal value, as a PHP string.
*/
protected function bcRound($value, $precision) {
if ($value[0] != '-') {
return bcadd($value, '0.' . str_repeat('0', $precision) . '5', $precision);
}
return bcsub($value, '0.' . str_repeat('0', $precision) . '5', $precision);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\fraction;
use Drupal\Core\TypedData\TypedData;
/**
* A computed property for storing the fraction decimal value for validation.
*/
class FractionDecimalProperty extends TypedData {
/**
* Cached value.
*
* @var string|null
*/
protected $decimal = NULL;
/**
* Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
*/
public function getValue($langcode = NULL) {
// If a value is already available, return it.
if ($this->decimal !== NULL) {
return $this->decimal;
}
// Load the parent item.
$item = $this->getParent();
// If numerator or denominator are null, the decimal is also null.
if (is_null($item->numerator) || is_null($item->denominator)) {
$this->decimal = NULL;
}
// Otherwise, create a Fraction object and generate decimal value with
// automatic precision.
else {
$fraction = new Fraction($item->numerator, $item->denominator);
$this->decimal = $fraction->toDecimal(0, TRUE);
}
return $this->decimal;
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
$this->decimal = $value;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Drupal\fraction;
/**
* Defines the interface for Fraction class.
*
* @package fraction
*/
interface FractionInterface {
/**
* Sets the numerator.
*
* @param string|int $numerator
* The numerator value.
*
* @return Fraction
* Returns this Fraction object.
*/
public function setNumerator($numerator);
/**
* Sets the denominator.
*
* @param string|int $denominator
* The denominator value.
*
* @return Fraction
* Returns this Fraction object.
*/
public function setDenominator($denominator);
/**
* Gets the numerator.
*
* @return string
* Returns the numerator value.
*/
public function getNumerator();
/**
* Gets the denominator.
*
* @return string
* Returns the denominator value.
*/
public function getDenominator();
/**
* Return a string representation of the fraction.
*
* @param string $separator
* The separator to place between the numerator and denominator.
*
* @return string
* Returns a string with the numerator, separator, and denominator.
*/
public function toString(string $separator = '/');
/**
* Calculates the decimal equivalent of the fraction.
*
* @param int $precision
* The desired decimal precision, defaults to 0.
* @param bool $auto_precision
* Boolean, whether or not the precision should be automatically calculated.
* This option provides more precision when you need it, and less when you
* don't. If set to TRUE, it will try to determine the maximum precision
* (this only works if the denominator is base 10). If the resulting
* precision is greater than $precision, it will be used instead.
*
* @return string
* Returns the decimal equivalent of the fraction as a PHP string.
*/
public function toDecimal(int $precision, bool $auto_precision = FALSE);
/**
* Calculates the numerator and denominator from a decimal value.
*
* @param string|int|float $value
* The decimal value to start with.
*
* @return Fraction
* Returns this object.
*/
public static function createFromDecimal($value);
/**
* Calculate the fraction's greatest common divisor using Euclid's algorithm.
*
* @return string
* Returns the greatest common divisor.
*/
public function gcd();
/**
* Reduces the fraction to its simplest form.
*
* @return Fraction
* Returns the reduced fraction as a new Fraction object.
*/
public function reduce();
/**
* Reciprocates the fraction.
*
* @return Fraction
* Returns the reciprocal as a new Fraction object.
*/
public function reciprocate();
/**
* Adds another fraction to this one.
*
* @param Fraction $fraction
* Another fraction object to add to this one.
*
* @return Fraction
* Returns the arithmetic result as a new Fraction object.
*/
public function add(Fraction $fraction);
/**
* Subtracts another fraction from this one.
*
* @param Fraction $fraction
* Another fraction object to subtract this one.
*
* @return Fraction
* Returns the arithmetic result as a new Fraction object.
*/
public function subtract(Fraction $fraction);
/**
* Multiplies this fraction with another one.
*
* @param Fraction $fraction
* Another fraction object to multiply with.
*
* @return Fraction
* Returns the arithmetic result as a new Fraction object.
*/
public function multiply(Fraction $fraction);
/**
* Divides this fraction by another one.
*
* @param Fraction $fraction
* Another fraction object to divide by.
*
* @return Fraction
* Returns the arithmetic result as a new Fraction object.
*/
public function divide(Fraction $fraction);
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\fraction;
use Drupal\Core\TypedData\TypedData;
/**
* A computed property for storing the fraction.
*/
class FractionProperty extends TypedData {
/**
* Cached Fraction object.
*
* @var Fraction|null
*/
protected $fraction = NULL;
/**
* Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
*/
public function getValue($langcode = NULL) {
// If a Fraction object is already available, return it.
if ($this->fraction !== NULL) {
return $this->fraction;
}
// Load the parent item.
$item = $this->getParent();
// If numerator or denominator are null, the fraction is also null.
if (is_null($item->numerator) || is_null($item->denominator)) {
$this->fraction = NULL;
}
// Otherwise, create a Fraction object with the numerator and denominator.
else {
$this->fraction = new Fraction($item->numerator, $item->denominator);
}
return $this->fraction;
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
$this->fraction = $value;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'fraction_decimal' formatter.
*
* @FieldFormatter(
* id = "fraction_decimal",
* label = @Translation("Decimal"),
* field_types = {
* "fraction"
* }
* )
*/
class FractionDecimalFormatter extends FractionFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'precision' => 0,
'auto_precision' => TRUE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
// Decimal precision.
$elements['precision'] = [
'#type' => 'number',
'#title' => $this->t('Decimal precision'),
'#description' => $this->t('Specify the number of digits after the decimal place to display. When "Auto precision" is enabled, this value essentially becomes a minimum fallback precision.'),
'#default_value' => $this->getSetting('precision'),
'#required' => TRUE,
'#weight' => 0,
];
// Auto precision.
$elements['auto_precision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Auto precision'),
'#description' => $this->t('Automatically determine the maximum precision if the fraction has a base-10 denominator. For example, 1/100 would have a precision of 2, 1/1000 would have a precision of 3, etc.'),
'#default_value' => $this->getSetting('auto_precision'),
'#weight' => 1,
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
// Summarize the precision setting.
$precision = $this->getSetting('precision');
$auto_precision = !empty($this->getSetting('auto_precision')) ? 'On' : 'Off';
$summary[] = $this->t('Precision: @precision, Auto-precision: @auto_precision', [
'@precision' => $precision,
'@auto_precision' => $auto_precision,
]);
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
// Output fraction as a decimal with a fixed or automatic precision.
$precision = $this->getSetting('precision');
$auto_precision = !empty($this->getSetting('auto_precision')) ? TRUE : FALSE;
// Iterate through the items.
foreach ($items as $delta => $item) {
/** @var \Drupal\fraction\FractionInterface $fraction */
$fraction = $item->fraction;
$output = $fraction->toDecimal($precision, $auto_precision);
$elements[$delta] = [
'#markup' => $this->viewOutput($item, $output),
];
}
return $elements;
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'fraction' formatter.
*
* @FieldFormatter(
* id = "fraction",
* label = @Translation("Fraction"),
* field_types = {
* "fraction"
* }
* )
*/
class FractionFormatter extends FractionFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'separator' => '/',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
// Numerator and denominator separator.
$elements['separator'] = [
'#type' => 'textfield',
'#title' => $this->t('Separator'),
'#description' => $this->t('Specify the separator to display between the numerator and denominator.'),
'#default_value' => $this->getSetting('separator'),
'#required' => TRUE,
'#weight' => 0,
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
// Summarize the separator setting.
$separator = $this->getSetting('separator');
$summary[] = $this->t('Separator: @separator', [
'@separator' => $separator,
]);
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
// Load the separator setting.
$separator = $this->getSetting('separator');
// Iterate through the items.
foreach ($items as $delta => $item) {
$output = $item->fraction->toString($separator);
$elements[$delta] = [
'#markup' => $this->viewOutput($item, $output),
];
}
return $elements;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Base class for fraction formatters.
*/
abstract class FractionFormatterBase extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'prefix_suffix' => FALSE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
$elements['prefix_suffix'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display prefix and suffix'),
'#default_value' => $this->getSetting('prefix_suffix'),
'#weight' => 10,
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if ($this->getSetting('prefix_suffix')) {
$summary[] = $this->t('Display with prefix and suffix.');
}
return $summary;
}
/**
* Account for extra output such as prefixes and suffixes.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* The field item to evaluate.
* @param string $output
* The output of the field.
*
* @return string
* The output with all relevant additions.
*/
protected function viewOutput(FieldItemInterface $item, $output = '') {
$field_settings = $this->getFieldSettings();
// Account for prefix and suffix.
if ($this->getSetting('prefix_suffix')) {
$prefixes = isset($field_settings['prefix']) ? array_map(['Drupal\Core\Field\FieldFilteredMarkup', 'create'], explode('|', $field_settings['prefix'])) : [''];
$suffixes = isset($field_settings['suffix']) ? array_map(['Drupal\Core\Field\FieldFilteredMarkup', 'create'], explode('|', $field_settings['suffix'])) : [''];
$prefix = (count($prefixes) > 1) ? $this->formatPlural($item->decimal, $prefixes[0], $prefixes[1]) : $prefixes[0];
$suffix = (count($suffixes) > 1) ? $this->formatPlural($item->decimal, $suffixes[0], $suffixes[1]) : $suffixes[0];
$output = $prefix . $output . $suffix;
}
return $output;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\fraction\Fraction;
/**
* Format fraction as percentage.
*
* @FieldFormatter(
* id = "fraction_percentage",
* label = @Translation("Percentage"),
* field_types = {
* "fraction"
* }
* )
*/
class PercentageFormatter extends FractionDecimalFormatter {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
// Iterate through the items.
foreach ($items as $delta => $item) {
/** @var \Drupal\fraction\Fraction $percentage */
$percentage = $item->fraction->multiply(Fraction::createFromDecimal('100'));
$auto_precision = !empty($this->getSetting('auto_precision'));
$output = $percentage->toDecimal($this->getSetting('precision'), $auto_precision) . '%';
$elements[$delta] = [
'#markup' => $this->viewOutput($item, $output),
];
}
return $elements;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\NumericItemBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\fraction\Fraction;
/**
* Plugin implementation of the 'fraction' field type.
*
* @FieldType(
* id = "fraction",
* label = @Translation("Fraction (two integers)"),
* description = @Translation("This field stores a decimal in fraction form (with a numerator and denominator) for maximum precision."),
* category = @Translation("Number"),
* default_widget = "fraction",
* default_formatter = "fraction",
* constraints = {"FractionConstraint" = {}}
* )
*/
class FractionItem extends NumericItemBase {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'numerator' => [
'description' => 'Fraction numerator value',
'type' => 'int',
'size' => 'big',
'not null' => FALSE,
],
'denominator' => [
'description' => 'Fraction denominator value',
'type' => 'int',
'not null' => FALSE,
],
],
];
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$element = parent::fieldSettingsForm($form, $form_state);
$element['min']['#step'] = 'any';
$element['max']['#step'] = 'any';
return $element;
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$property_definitions['numerator'] = DataDefinition::create('integer')
->setLabel(t('Numerator value'));
$property_definitions['denominator'] = DataDefinition::create('integer')
->setLabel(t('Denominator value'));
$property_definitions['fraction'] = MapDataDefinition::create()
->setLabel(t('Fraction'))
->setDescription(t('A fraction object instance.'))
->setComputed(TRUE)
->setClass('\Drupal\fraction\FractionProperty');
$property_definitions['decimal'] = MapDataDefinition::create()
->setLabel(t('Fraction Decimal'))
->setDescription(t('Fraction decimal value.'))
->setComputed(TRUE)
->setInternal(FALSE)
->setClass('\Drupal\fraction\FractionDecimalProperty');
return $property_definitions;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$numerator = $this->get('numerator')->getValue();
$denominator = $this->get('denominator')->getValue();
return (((string) $numerator !== '0' && empty($numerator)) || ((string) $denominator !== '0' && empty($denominator)));
}
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
// Determine if we populate the fraction numerator and denominator field
// properties from a single decimal value.
$decimal = NULL;
// Treat the values as a decimal if no array is given.
if (isset($values) && !is_array($values)) {
$decimal = $values;
}
// If the decimal property is specified, use it.
if (is_array($values) && isset($values['decimal'])) {
$decimal = $values['decimal'];
}
// Populate the fraction field if decimal is numeric.
if (is_numeric($decimal) && $fraction = Fraction::createFromDecimal($decimal)) {
$values = [
'numerator' => $fraction->getNumerator(),
'denominator' => $fraction->getDenominator(),
];
}
parent::setValue($values, $notify);
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
/** @var \Symfony\Component\Validator\Constraint[] $constraints */
$constraints = parent::getConstraints();
foreach ($constraints as &$constraint) {
// Replace 'value' with 'decimal' in min/max range constraints.
if (!empty($constraint->properties['value'])) {
$constraint->properties['decimal'] = $constraint->properties['value'];
unset($constraint->properties['value']);
}
}
return $constraints;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// Generate random decimal (float) with a max of 9 decimal places and then
// convert it to fraction.
$divisor = pow(10, rand(0, 9));
$number = mt_rand(1, 20 * $divisor) / $divisor;
$fraction = Fraction::createFromDecimal($number);
return [
'numerator' => $fraction->getNumerator(),
'denominator' => $fraction->getDenominator(),
];
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return NULL;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'fraction_decimal' widget.
*
* @FieldWidget(
* id = "fraction_decimal",
* label = @Translation("Decimal"),
* field_types = {
* "fraction"
* }
* )
*/
class FractionDecimalWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'precision' => 0,
'auto_precision' => TRUE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
// Decimal precision.
$elements['precision'] = [
'#type' => 'number',
'#title' => $this->t('Decimal precision'),
'#description' => $this->t('Specify the number of digits after the decimal place to display when converting the fraction to a decimal. When "Auto precision" is enabled, this value essentially becomes a minimum fallback precision.'),
'#default_value' => $this->getSetting('precision'),
'#required' => TRUE,
'#weight' => 0,
];
// Auto precision.
$elements['auto_precision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Auto precision'),
'#description' => $this->t('Automatically determine the maximum precision if the fraction has a base-10 denominator. For example, 1/100 would have a precision of 2, 1/1000 would have a precision of 3, etc.'),
'#default_value' => $this->getSetting('auto_precision'),
'#weight' => 1,
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
// Summarize the precision setting.
$precision = $this->getSetting('precision');
$auto_precision = !empty($this->getSetting('auto_precision')) ? 'On' : 'Off';
$summary[] = $this->t('Precision: @precision, Auto-precision: @auto_precision', [
'@precision' => $precision,
'@auto_precision' => $auto_precision,
]);
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
// Add a 'decimal' textfield for capturing the decimal value.
// The default value is converted to a decimal with the specified precision.
$precision = $this->getSetting('precision');
$auto_precision = !empty($this->getSetting('auto_precision')) ? TRUE : FALSE;
$element['decimal'] = $element + [
'#type' => 'fraction_decimal',
'#default_value' => $items->isEmpty() ? '' : $items[$delta]->fraction->toDecimal($precision, $auto_precision),
];
$field_settings = $this->getFieldSettings();
// Set minimum and maximum.
if (isset($field_settings['min']) && is_numeric($field_settings['min'])) {
$element['#min'] = $field_settings['min'];
}
if (isset($field_settings['max']) && is_numeric($field_settings['max'])) {
$element['#max'] = $field_settings['max'];
}
// Add prefix and suffix.
if ($field_settings['prefix']) {
$prefixes = explode('|', $field_settings['prefix']);
$element['decimal']['#field_prefix'] = FieldFilteredMarkup::create(array_pop($prefixes));
}
if ($field_settings['suffix']) {
$suffixes = explode('|', $field_settings['suffix']);
$element['decimal']['#field_suffix'] = FieldFilteredMarkup::create(array_pop($suffixes));
}
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
foreach ($values as &$value) {
// Remove the fraction form element wrapper if it exists to make the
// FractionItem::setValue method more consistent.
$value = $value['decimal'] ?? $value;
}
return $values;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\fraction\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'fraction' widget.
*
* @FieldWidget(
* id = "fraction",
* label = @Translation("Fraction"),
* field_types = {
* "fraction"
* }
* )
*/
class FractionWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element += [
'#type' => 'details',
'#open' => TRUE,
];
$element['fraction'] = [
'#type' => 'fraction',
'#title' => $this->t('Fraction'),
'#default_value' => [
'numerator' => $items[$delta]->numerator ?? NULL,
'denominator' => $items[$delta]->denominator ?? NULL,
],
];
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
foreach ($values as &$value) {
// Remove the fraction form element wrapper if it exists to make the
// FractionItem::setValue method more consistent.
$value = $value['fraction'] ?? $value;
}
return $values;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\fraction\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Plugin implementation of the 'fraction_constraint'.
*
* @Constraint(
* id = "FractionConstraint",
* label = @Translation("Fraction constraint", context = "Validation"),
* )
*/
class FractionConstraint extends Constraint {
/**
* Constraint message if denominator is empty or zero.
*
* @var string
*/
public $denominatorNotZero = 'The denominator of a fraction cannot be zero or empty.';
/**
* Constraint message if denominator is out of range from 1 to 2147483647.
*
* @var string
*/
public $denominatorOutOfRange = 'The denominator must be between 1 and 2147483647';
/**
* Constraint message if numerator is out of range.
*
* Range spans from -9223372036854775808 to 9223372036854775807
*
* @var string
*/
public $numeratorOutOfRange = 'The numerator must be between -9223372036854775808 and 9223372036854775807.';
/**
* Constraint message if number of decimal digits is greater than 9.
*
* @var string
*/
public $maxNumberOfDecimals = 'The maximum number of digits after the decimal place is 9.';
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\fraction\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the FractionConstraint constraint.
*/
class FractionConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($items, Constraint $constraint) {
/** @var \Drupal\fraction\Plugin\Field\FieldType\FractionItem $fraction */
$fractionItem = $items;
$values = $fractionItem->getValue();
if (empty($values['denominator']) && !empty($values['numerator'])) {
$this->context->addViolation($constraint->denominatorNotZero);
return;
}
if (!empty($values['denominator']) && ((string) $values['denominator'] <= '0' || (string) $values['denominator'] > '2147483647')) {
$this->context->addViolation($constraint->denominatorOutOfRange);
return;
}
if (!empty($values['numerator']) && ((string) $values['numerator'] < '-9223372036854775808' || (string) $values['numerator'] > '9223372036854775807')) {
$this->context->addViolation($constraint->numeratorOutOfRange);
return;
}
if ((string) $values['denominator'] > '1000000000') {
$this->context->addViolation($constraint->maxNumberOfDecimals);
return;
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\fraction\Plugin\migrate\field\d7;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* Migrate from d7 fraction structure.
*
* @MigrateField(
* id = "fraction",
* core = {7},
* source_module = "fraction",
* destination_module = "fraction"
* )
*/
class FractionField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'fraction_default' => 'fraction',
'fraction_decimal' => 'fraction_decimal',
'fraction_percentage' => 'fraction_percentage',
];
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'fraction_default' => 'fraction',
'fraction_decimal' => 'fraction_decimal',
'fraction_percentage' => 'fraction_percentage',
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\fraction\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\fraction\Fraction;
/**
* Provides a 'DecimalFraction' migrate process plugin.
*
* @MigrateProcessPlugin(
* id = "decimal_fraction",
* handle_multiples = TRUE
* )
*
* Extracts the numerator and denominator that can be directly mapped to a
* Fraction field from a decimal input.
*
* Example:
*
* @code
* fraction:
* plugin: decimal_fraction
* source: decimal
* @endcode
*/
class DecimalFraction extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!is_numeric($value) || $value === '') {
return [
'numerator' => '',
'denominator' => '',
];
}
else {
$fraction = Fraction::createFromDecimal($value);
return [
'numerator' => $fraction->getNumerator(),
'denominator' => $fraction->getDenominator(),
];
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Drupal\fraction\Plugin\views\field;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ResultRow;
use Drupal\fraction\Fraction as FractionClass;
/**
* Field handler for Fraction database columns.
*
* @ingroup views_field_handlers
*
* @ViewsField("fraction")
*/
class Fraction extends FieldPluginBase {
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
// Default to automatic precision.
$options['precision'] = ['default' => 0];
$options['auto_precision'] = ['default' => TRUE];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
// Add fields for configuring precision and auto_precision.
$form['precision'] = [
'#type' => 'textfield',
'#title' => $this->t('Precision'),
'#description' => $this->t('Specify the number of digits after the decimal place to display when converting the fraction to a decimal. When "Auto precision" is enabled, this value essentially becomes a minimum fallback precision.'),
'#default_value' => $this->options['precision'],
];
$form['auto_precision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Auto precision'),
'#description' => $this->t('Automatically determine the maximum precision if the fraction has a base-10 denominator. For example, 1/100 would have a precision of 2, 1/1000 would have a precision of 3, etc.'),
'#default_value' => $this->options['auto_precision'],
];
// Merge into the parent form.
parent::buildOptionsForm($form, $form_state);
// Remove the 'click_sort_column' form element, because we provide a custom
// click_sort function below to use the numerator and denominator columns
// simultaneously.
unset($form['click_sort_column']);
}
/**
* {@inheritdoc}
*/
public function clickSort($order) {
// Ensure the main table for this field is included.
$this->ensureMyTable();
// Formula for calculating the final value, by dividing numerator by
// denominator.
// These are available as additional fields.
$numerator = $this->tableAlias . '.' . $this->definition['additional fields']['numerator'];
$denominator = $this->tableAlias . '.' . $this->definition['additional fields']['denominator'];
// Multiply the numerator field by 1.0 so the database returns a decimal
// from the computation.
$formula = '1.0 * ' . $numerator . ' / ' . $denominator;
// Add the orderby.
$this->query->addOrderBy(NULL, $formula, $order, $this->tableAlias . '_decimal');
}
/**
* Loads the numerator and denominator values and converts to decimal.
*/
public function getValue(ResultRow $values, $field = NULL) {
// Find the numerator and denominator field aliases.
$numerator_alias = $this->aliases[$this->definition['additional fields']['numerator']];
$denominator_alias = $this->aliases[$this->definition['additional fields']['denominator']];
// If both values are available...
if (isset($values->{$numerator_alias}) && isset($values->{$denominator_alias})) {
// Convert to decimal.
$numerator = $values->{$numerator_alias};
$denominator = $values->{$denominator_alias};
$precision = $this->options['precision'];
$auto_precision = $this->options['auto_precision'];
$fraction = new FractionClass($numerator, $denominator);
return $fraction->toDecimal($precision, $auto_precision);
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\fraction\Plugin\views\field;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\Core\Form\FormStateInterface;
/**
* Field handler for Fraction fields.
*
* Overrides the clickSort() method to use a formula that divides
* the numerator by the denominator.
*
* Overrides the buildOptionsForm() method to remove the 'click_sort_column'
* form element.
*
* @ingroup views_field_handlers
*
* @ViewsField("fraction_field")
*/
class FractionField extends EntityField {
/**
* {@inheritdoc}
*/
public function clickSort($order) {
// Ensure the main table for this field is included.
$this->ensureMyTable();
// Formula for calculating the final value, by dividing numerator by
// denominator. These are available as additional fields.
$numerator = $this->tableAlias . '.' . $this->field . '_numerator';
$denominator = $this->tableAlias . '.' . $this->field . '_denominator';
// Multiply the numerator field by 1.0 so the database returns a decimal
// from the computation.
$formula = '1.0 * ' . $numerator . ' / ' . $denominator;
// Add the orderby.
$this->query->addOrderBy(NULL, $formula, $order, $this->tableAlias . '_decimal');
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
// Inherit the parent options form.
parent::buildOptionsForm($form, $form_state);
// Remove the 'click_sort_column' form element.
unset($form['click_sort_column']);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\fraction\Plugin\views\filter;
use Drupal\views\Plugin\views\filter\NumericFilter;
/**
* Filter handler for Fraction fields.
*
* Overrides query function to use a formula which divides the numerator
* by the denominator.
*
* Overrides operator functions (op_between, op_simple, and op_regex).
* Alter the add_where() method call in each to use the formula.
* Note that op_empty() is not included because we are not setting
* 'allow empty'.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("fraction")
*/
class Fraction extends NumericFilter {
/**
* {@inheritdoc}
*/
public function query() {
// Ensure the main table for this field is included.
$this->ensureMyTable();
// Formula for calculating the final value, by dividing numerator by
// denominator. These are added as additional fields in
// hook_field_views_data_alter().
$numerator = $this->tableAlias . '.' . $this->definition['additional fields']['numerator'];
$denominator = $this->tableAlias . '.' . $this->definition['additional fields']['denominator'];
// Multiply the numerator field by 1.0 so the database returns a decimal
// from the computation.
$formula = '1.0 * ' . $numerator . ' / ' . $denominator;
// Perform the filter using the selected operator and the formula.
$info = $this->operators();
if (!empty($info[$this->operator]['method'])) {
$this->{$info[$this->operator]['method']}($formula);
}
}
/**
* {@inheritdoc}
*/
protected function opBetween($field) {
if ($this->operator == 'between') {
$expression = $field . ' BETWEEN :min AND :max';
$this->query->addWhereExpression($this->options['group'], $expression, [':min' => $this->value['min'], ':max' => $this->value['max']]);
}
else {
$expression = $field . ' <= :min OR ' . $field . ' >= :max';
$this->query->addWhereExpression($this->options['group'], $expression, [':min' => $this->value['min'], ':max' => $this->value['max']]);
}
}
/**
* {@inheritdoc}
*/
protected function opSimple($field) {
$expression = $field . ' ' . $this->operator . ' :value';
$this->query->addWhereExpression($this->options['group'], $expression, [':value' => $this->value['value']]);
}
/**
* {@inheritdoc}
*/
protected function opRegex($field) {
$expression = $field . ' RLIKE :value';
$this->query->addWhereExpression($this->options['group'], $expression, [':value' => $this->value['value']]);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\fraction\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\SortPluginBase;
/**
* Sort handler for Fraction fields.
*
* Overrides query function to use a formula which divides the numerator
* by the denominator.
*
* @ingroup views_sort_handlers
*
* @ViewsSort("fraction")
*/
class Fraction extends SortPluginBase {
/**
* {@inheritdoc}
*/
public function query() {
// Ensure the main table for this field is included.
$this->ensureMyTable();
// Formula for calculating the final value, by dividing numerator by
// denominator. These are added as additional fields in
// hook_field_views_data_alter().
$numerator = $this->tableAlias . '.' . $this->definition['additional fields']['numerator'];
$denominator = $this->tableAlias . '.' . $this->definition['additional fields']['denominator'];
// Multiply the numerator field by 1.0 so the database returns a decimal
// from the computation.
$formula = '1.0 * ' . $numerator . ' / ' . $denominator;
// Add the orderby.
$this->query->addOrderBy(NULL, $formula, $this->options['order'], $this->tableAlias . '_decimal');
}
}

View File

@@ -0,0 +1,14 @@
name: Fraction test
description: 'Support module for fraction testing.'
type: module
package: Testing
core_version_requirement: ^9
dependencies:
- drupal:node
- drupal:user
- fraction:fraction
# Information added by Drupal.org packaging script on 2023-08-21
version: '2.3.1'
project: 'fraction'
datestamp: 1692651922

View File

@@ -0,0 +1,23 @@
<?php
/**
* @file
* Fraction test module.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Implements hook_entity_base_field_info().
*/
function fraction_test_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];
// Add a single-value fraction base field to nodes.
if ($entity_type->id() == 'node') {
$fields['fraction'] = BaseFieldDefinition::create('fraction')->setLabel('Fraction')->setCardinality(1);
}
return $fields;
}

View File

@@ -0,0 +1,391 @@
<?php
namespace Drupal\Tests\fraction\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Tests the creation of fraction fields.
*
* @group Fraction
*/
class FractionFieldTest extends BrowserTestBase {
use StringTranslationTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'entity_test', 'field_ui', 'fraction'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'view test entity',
'administer entity_test content',
'administer content types',
'administer node fields',
'administer node display',
'administer node form display',
'bypass node access',
'administer entity_test fields',
'access administration pages',
]));
}
/**
* Test decimal widget field.
*/
public function testFractionWidgetDecimal() {
$max = 100;
$min = 10;
// Create a field with settings to validate.
$field_name = mb_strtolower($this->randomMachineName());
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'fraction',
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'settings' =>
[
'max' => $max,
'min' => $min,
],
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction_decimal'])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction_decimal'])
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldValueEquals("{$field_name}[0][decimal]", '');
// Submit decimal value.
$value = '14.5678';
$edit = [
"{$field_name}[0][decimal]" => $value,
];
$this->submitForm($edit, $this->t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
$id = $match[1];
$this->assertSession()->pageTextContains($this->t('entity_test @id has been created.', ['@id' => $id]));
$this->assertSession()->responseContains($value);
// Try to create entries with more than one decimal separator; assert fail.
$wrong_entries = [
'3.14.159',
'0..45469',
'..4589',
'6.459.52',
'6.3..25',
];
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][decimal]" => $wrong_entry,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('%name must be a number.', ['%name' => $field_name]));
}
// Try to create entries with minus sign not in the first position.
$wrong_entries = [
'3-3',
'4-',
'1.3-',
'1.2-4',
'-10-10',
];
foreach ($wrong_entries as $wrong_entry) {
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][decimal]" => $wrong_entry,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('%name must be a number.', ['%name' => $field_name]));
}
// Try to set a value above the maximum value.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][decimal]" => $max + 0.123,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('%name: the value may be no greater than %maximum.', ['%name' => $field_name, '%maximum' => $max]));
// Try to set a value below the minimum value.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][decimal]" => $min - 0.123,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('%name: the value may be no less than %minimum.', ['%name' => $field_name, '%minimum' => $min]));
// Test the fraction decimal element limits.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][decimal]" => 10.1234567891,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('The maximum number of digits after the decimal place is 9.'));
}
/**
* Test decimal widget field with negative values.
*/
public function testFractionWidgetDecimalNegative() {
// Create the test field.
$field_name = mb_strtolower($this->randomMachineName());
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'fraction',
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction_decimal'])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction_decimal'])
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldValueEquals("{$field_name}[0][decimal]", '');
// Submit negative decimal value.
$value = '-14.5678';
$edit = [
"{$field_name}[0][decimal]" => $value,
];
$this->submitForm($edit, $this->t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
$id = $match[1];
$this->assertSession()->pageTextContains($this->t('entity_test @id has been created.', ['@id' => $id]));
$this->assertSession()->responseContains($value);
}
/**
* Test decimal widget field.
*/
public function testFractionWidgetFraction() {
$max = 100;
$min = 10;
// Create a field with settings to validate.
$field_name = mb_strtolower($this->randomMachineName());
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'fraction',
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
'settings' =>
[
'max' => $max,
'min' => $min,
],
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction'])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction'])
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldValueEquals("{$field_name}[0][fraction][numerator]", '');
$this->assertSession()->fieldValueEquals("{$field_name}[0][fraction][denominator]", '');
// Submit fraction value.
$edit = [
"{$field_name}[0][fraction][numerator]" => 150,
"{$field_name}[0][fraction][denominator]" => 10,
];
$this->submitForm($edit, $this->t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
$id = $match[1];
$this->assertSession()->pageTextContains($this->t('entity_test @id has been created.', ['@id' => $id]));
$this->assertSession()->responseContains('150');
$this->assertSession()->responseContains('10');
// Try to set a value above the maximum value.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => 15000,
"{$field_name}[0][fraction][denominator]" => 10,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('%name: the value may be no greater than %maximum.', ['%name' => $field_name, '%maximum' => $max]));
// Try to set a value below the minimum value.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => 1,
"{$field_name}[0][fraction][denominator]" => 10,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('%name: the value may be no less than %minimum.', ['%name' => $field_name, '%minimum' => $min]));
// Empty denominator.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => 1,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('The denominator of a fraction cannot be zero or empty (if a numerator is provided).'));
// Numerators must be between -9223372036854775808 and 9223372036854775807.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => '-9223372036854775809',
"{$field_name}[0][fraction][denominator]" => 10,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('The numerator of a fraction must be between -9223372036854775808 and 9223372036854775807.'));
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => '9223372036854775808',
"{$field_name}[0][fraction][denominator]" => 10,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('The numerator of a fraction must be between -9223372036854775808 and 9223372036854775807.'));
// Denominators must be between 0 and 2147483647.
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => 10,
"{$field_name}[0][fraction][denominator]" => -1,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('The denominator of a fraction must be greater than 0 and less than 2147483647.'));
$this->drupalGet('entity_test/add');
$edit = [
"{$field_name}[0][fraction][numerator]" => 10,
"{$field_name}[0][fraction][denominator]" => 2147483648,
];
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseContains($this->t('The denominator of a fraction must be greater than 0 and less than 2147483647.'));
}
/**
* Test min/max with decimal values.
*/
public function testFractionWidgetMinMaxDecimal() {
$this->drupalCreateContentType(['type' => 'article']);
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('/admin/structure/types/manage/article/fields/add-field');
$page->selectFieldOption('new_storage_type', 'fraction');
$page->fillField('label', 'Fraction field');
$page->fillField('field_name', 'fraction_field');
$page->pressButton('Save and continue');
$page->pressButton('Save field settings');
$page->fillField('settings[min]', 10.5);
$page->fillField('settings[max]', 100.5);
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved Fraction field configuration.');
}
/**
* Tests that editing an empty fraction doesn't show a 0.
*
* @see https://www.drupal.org/project/fraction/issues/3096236
*/
public function testFractionWidgetDecimalEditNull() {
// Create a field with settings to validate.
$field_name = mb_strtolower($this->randomMachineName());
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'fraction',
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction_decimal'])
->save();
$display_repository->getViewDisplay('entity_test', 'entity_test')
->setComponent($field_name, ['type' => 'fraction_decimal'])
->save();
// Display creation form.
$this->drupalGet('entity_test/add');
$this->assertSession()->fieldValueEquals("{$field_name}[0][decimal]", '');
// Submit decimal value.
$value = '1.234';
$edit = [
"{$field_name}[0][decimal]" => $value,
];
$this->submitForm($edit, $this->t('Save'));
preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
$id = $match[1];
$this->assertSession()->pageTextContains($this->t('entity_test @id has been created.', ['@id' => $id]));
$this->assertSession()->responseContains($value);
// Empty the field.
$edit = [
"{$field_name}[0][decimal]" => NULL,
];
$this->drupalGet("entity_test/manage/$id/edit");
$this->submitForm($edit, $this->t('Save'));
$this->assertSession()->responseNotContains($value);
// The field should have no value (not 0, just empty).
$this->drupalGet("entity_test/manage/$id/edit");
$xpath = $this->assertSession()->buildXPathQuery('//input[@name=:value]', [':value' => "{$field_name}[0][decimal]"]);
$elements = $this->xpath($xpath);
$element = reset($elements);
$this->assertSame($element->getValue(), '', 'Field is empty');
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\Tests\fraction\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\fraction\Fraction;
use Drupal\fraction\Plugin\Field\FieldType\FractionItem;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\field\Kernel\FieldKernelTestBase;
/**
* Class to test the FractionItem field.
*
* @group Fraction
*/
class FractionFieldTest extends FieldKernelTestBase {
/**
* Field name to use.
*/
const FIELD_NAME = 'field_fraction_test';
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'fraction'];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installSchema('node', ['node_access']);
NodeType::create([
'type' => 'article',
'label' => 'Article',
])->save();
$this->fieldTestData->{$this::FIELD_NAME . '_storage'} = FieldStorageConfig::create([
'entity_type' => 'node',
'field_name' => self::FIELD_NAME,
'type' => 'fraction',
]);
$this->fieldTestData->{$this::FIELD_NAME . '_storage'}->save();
$this->fieldTestData->{$this::FIELD_NAME} = FieldConfig::create([
'entity_type' => 'node',
'field_name' => self::FIELD_NAME,
'bundle' => 'article',
]);
$this->fieldTestData->{$this::FIELD_NAME}->save();
}
/**
* Test field properties.
*/
public function testFieldProperties() {
// Check for the correct main property name.
$this->assertNull(FractionItem::mainPropertyName(), 'The fraction item main property name is NULL.');
// Check for correct property definitions.
$definitions = FractionItem::propertyDefinitions($this->fieldTestData->{$this::FIELD_NAME . '_storage'});
$properties = ['numerator', 'denominator', 'fraction', 'decimal'];
foreach ($properties as $property) {
$this->assertNotEmpty($definitions[$property], "The fraction item has a $property property.");
}
// Ensure the decimal property is not internal to be included in JSON:API.
$this->assertFalse($definitions['decimal']->isInternal(), 'The fraction item decimal property is not internal.');
}
/**
* Tests setting the fraction field item from a decimal.
*/
public function testSetValue() {
// Setup the entity and field.
$node = Node::create([
'type' => 'article',
'title' => 'test',
]);
$node->save();
$fraction_field = $node->get(self::FIELD_NAME);
// Confirm that an empty field returns a null values.
$fraction_field->set(0, NULL);
$this->assertEquals(NULL, $fraction_field->first()->numerator);
$this->assertEquals(NULL, $fraction_field->first()->denominator);
$this->assertEquals(NULL, $fraction_field->first()->get('fraction')->getValue());
$this->assertEquals(NULL, $fraction_field->first()->get('decimal')->getValue());
// Set value with a decimal.
$fraction_field->set(0, 1.2);
$this->assertEquals(12, $fraction_field->first()->numerator);
$this->assertEquals(10, $fraction_field->first()->denominator);
$this->assertEquals('12/10', $fraction_field->first()->get('fraction')->getValue()->toString());
$this->assertEquals('1.2', $fraction_field->first()->get('decimal')->getValue());
// Change the value.
$fraction_field->set(0, 12.3);
$this->assertEquals(123, $fraction_field->first()->numerator);
$this->assertEquals(10, $fraction_field->first()->denominator);
$this->assertEquals('123/10', $fraction_field->first()->get('fraction')->getValue()->toString());
$this->assertEquals('12.3', $fraction_field->first()->get('decimal')->getValue());
// Set value with the decimal property.
$fraction_field->set(0, ['decimal' => 45.6]);
$this->assertEquals(456, $fraction_field->first()->numerator);
$this->assertEquals(10, $fraction_field->first()->denominator);
$this->assertEquals('456/10', $fraction_field->first()->get('fraction')->getValue()->toString());
$this->assertEquals('45.6', $fraction_field->first()->get('decimal')->getValue());
}
/**
* Tests setting the sample value.
*/
public function testGenerateSampleValue() {
$node = Node::create([
'type' => 'article',
'title' => 'test',
]);
$node->save();
$node->get(self::FIELD_NAME)->generateSampleItems();
$this->entityValidateAndSave($node);
$result = $node->get(self::FIELD_NAME)->getValue();
$result = reset($result);
$this->assertTrue(is_array($result));
$this->assertArrayHasKey('numerator', $result);
$this->assertArrayHasKey('denominator', $result);
$this->assertTrue($result['denominator'] > 0, TRUE);
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace Drupal\Tests\fraction\Kernel;
use Drupal\Core\Database\Database;
use Drupal\Core\Utility\UpdateException;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\node\Entity\NodeType;
/**
* Provides tests for updating the schema from signed to unsigned.
*
* @group Fraction
*/
class FractionUpdateTest extends EntityKernelTestBase {
/**
* Connection to the database.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Database schema instance.
*
* @var \Drupal\Core\Database\Schema
*/
protected $schema;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'fraction',
'fraction_test',
'node',
];
/**
* Fields to check the update.
*
* @var array
*/
protected $fieldsToUpdate;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->connection = Database::getConnection();
$this->schema = $this->connection->schema();
$node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
$node_type->save();
FieldStorageConfig::create([
'field_name' => 'field_fraction_node',
'entity_type' => 'node',
'type' => 'fraction',
'cardinality' => -1,
])->save();
FieldConfig::create([
'field_name' => 'field_fraction_node',
'entity_type' => 'node',
'bundle' => 'article',
'label' => $this->randomMachineName() . '_label',
])->save();
FieldStorageConfig::create([
'field_name' => 'field_fraction_user',
'entity_type' => 'user',
'type' => 'fraction',
'cardinality' => -1,
])->save();
FieldConfig::create([
'field_name' => 'field_fraction_user',
'entity_type' => 'user',
'bundle' => 'user',
'label' => $this->randomMachineName() . '_label',
])->save();
$this->fieldsToUpdate = [
[
'table_name' => 'node_field_data',
'columns' => [
'numerator' => 'fraction__numerator',
'denominator' => 'fraction__denominator',
],
],
[
'table_name' => 'node__field_fraction_node',
'columns' => [
'numerator' => 'field_fraction_node_numerator',
'denominator' => 'field_fraction_node_denominator',
],
],
[
'table_name' => 'user__field_fraction_user',
'columns' => [
'numerator' => 'field_fraction_user_numerator',
'denominator' => 'field_fraction_user_denominator',
],
],
];
// Revert all the fields (with no data) to be unsigned so the post_update
// hook can be run.
foreach ($this->fieldsToUpdate as $field) {
$this->schema->changeField($field['table_name'], $field['columns']['denominator'], $field['columns']['denominator'], [
'type' => 'int',
'not null' => TRUE,
'unsigned' => TRUE,
'default' => 1,
]);
}
}
/**
* Tests the denominator update.
*/
public function testUpdateDenominatorSigned() {
// Unsigned fields should reject negative values.
foreach ($this->fieldsToUpdate as $field) {
if ($field['table_name'] == 'node_field_data') {
continue;
}
$this->assertFalse($this->tryUnsignedInsert($field['table_name'], $field['columns']), 'Column rejected a negative value.');
}
// Run the fraction update only.
$post_update_registry = $this->container->get('update.post_update_registry');
foreach ($post_update_registry->getUpdateFunctions('fraction') as $function) {
$function();
}
// Once the field is changed, negative values should be accepted. There's
// no unintrusive way to consistently check the actual SQL schema, so
// checking that there's a change on behaviour regarding signed/unsigned is
// the fastest way to check.
foreach ($this->fieldsToUpdate as $field) {
if ($field['table_name'] == 'node_field_data') {
continue;
}
$this->assertTrue($this->tryUnsignedInsert($field['table_name'], $field['columns']), 'Column accepted a negative value.');
}
}
/**
* Tests that data over the limit triggers a message on update.
*/
public function testUpdateDenominatorSignedException() {
// Unsigned fields should reject negative values.
foreach ($this->fieldsToUpdate as $field) {
if ($field['table_name'] == 'node_field_data') {
continue;
}
$this->assertFalse($this->tryUnsignedInsert($field['table_name'], $field['columns']), 'Column rejected a negative value.');
}
// Insert a value higher than the signed int bounds so that, when checked
// will trigger the update exception.
$this->container->get('database')->insert('node__field_fraction_node')
->fields(
[
'entity_id' => 1,
'revision_id' => 1,
'delta' => 0,
'field_fraction_node_numerator' => 9,
'field_fraction_node_denominator' => 2147483699,
]
)
->execute();
try {
// Run the fraction update only.
$post_update_registry = $this->container->get('update.post_update_registry');
foreach ($post_update_registry->getUpdateFunctions('fraction') as $function) {
$function();
}
// If the code reach this point, means that the update succeeded.
$this->fail('Failed due to update going through when it should not.');
}
catch (UpdateException $e) {
foreach ($this->fieldsToUpdate as $field) {
if ($field['table_name'] == 'node_field_data') {
continue;
}
$this->assertFalse($this->tryUnsignedInsert($field['table_name'], $field['columns']), 'Column rejected a negative value.');
}
}
}
/**
* Tries to insert a negative value into columns defined as unsigned.
*
* @see \Drupal\KernelTests\Core\Database\SchemaTest::tryUnsignedInsert()
*/
public function tryUnsignedInsert($table_name, $columns) {
try {
$this->container->get('database')->insert($table_name)
->fields(
[
'entity_id' => 1,
'revision_id' => 1,
'delta' => 0,
$columns['numerator'] => 1,
$columns['denominator'] => -10,
]
)
->execute();
return TRUE;
}
catch (\Exception $e) {
return FALSE;
}
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Drupal\Tests\fraction\Kernel\Plugin\Constraints;
use Drupal\Core\Entity\EntityConstraintViolationList;
use Drupal\Core\Validation\Plugin\Validation\Constraint\PrimitiveTypeConstraint;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\fraction\Fraction;
use Drupal\fraction\Plugin\Validation\Constraint\FractionConstraint;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\node\Entity\NodeType;
/**
* Tests Constraint for Fraction.
*
* @group Fraction
*/
class FractionConstraintTest extends EntityKernelTestBase {
/**
* {@inheritDoc}
*/
protected static $modules = ['node', 'fraction'];
/**
* EntityInterface.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* Field name.
*
* @var string
*/
const FIELD_NAME = 'field_fraction_test';
/**
* {@inheritDoc}
*/
protected function setUp(): void {
parent::setUp();
NodeType::create([
'type' => 'article',
'label' => 'Article',
])->save();
FieldStorageConfig::create([
'entity_type' => 'node',
'field_name' => self::FIELD_NAME,
'type' => 'fraction',
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => 'node',
'field_name' => self::FIELD_NAME,
'bundle' => 'article',
])->save();
$this->entity = $this->entityTypeManager->getStorage('node')->create([
'type' => 'article',
'title' => 'test',
self::FIELD_NAME => [
'numerator' => 1,
'denominator' => 1,
],
]);
}
/**
* Tests constraints.
*/
public function testConstraints() {
$violations = $this->entity->validate();
$this->assertCount(0, $violations);
$this->assertViolations($violations);
$this->entity->{self::FIELD_NAME} = ['denominator' => '0', 'numerator' => '1'];
$violations = $this->entity->validate();
$this->assertCount(1, $violations);
$this->entity->{self::FIELD_NAME} = ['denominator' => '2147483649', 'numerator' => '1'];
$violations = $this->entity->validate();
$this->assertCount(1, $violations);
$this->assertViolations($violations);
$this->entity->{self::FIELD_NAME} = ['denominator' => '123456', 'numerator' => '1'];
$violations = $this->entity->validate();
$this->assertCount(0, $violations);
$this->entity->{self::FIELD_NAME} = ['denominator' => '1', 'numerator' => '9223372036854775808'];
$violations = $this->entity->validate();
$this->assertCount(2, $violations);
$this->assertViolations($violations);
$this->entity->{self::FIELD_NAME} = ['denominator' => '1', 'numerator' => '-9223372036854775809'];
$violations = $this->entity->validate();
$this->assertCount(2, $violations);
$this->assertViolations($violations);
$this->entity->{self::FIELD_NAME} = ['denominator' => '1', 'numerator' => '92233720368547758'];
$violations = $this->entity->validate();
$this->assertCount(0, $violations);
$this->entity->{self::FIELD_NAME} = ['decimal' => '1.255'];
$violations = $this->entity->validate();
$this->assertCount(0, $violations);
$decimal = '9223372.993685481231231';
$this->entity->{self::FIELD_NAME} = $this->generateFraction($decimal);
$violations = $this->entity->validate();
$this->assertCount(2, $violations);
$this->assertViolations($violations);
$decimal = '11.1234';
$this->entity->{self::FIELD_NAME} = $this->generateFraction($decimal);
$violations = $this->entity->validate();
$this->assertCount(0, $violations);
$decimal = '8.1251251251';
$this->entity->{self::FIELD_NAME} = $this->generateFraction($decimal);
$violations = $this->entity->validate();
$this->assertCount(1, $violations);
$this->assertViolations($violations);
$decimal = '8.12512512';
$this->entity->{self::FIELD_NAME} = $this->generateFraction($decimal);
$violations = $this->entity->validate();
$this->assertCount(0, $violations);
}
/**
* Helper method that generates fraction from decimal to set it on field.
*
* @param string $decimal
* Decimal to convert to fraction.
*
* @return array
* Array to be set on field.
*/
protected function generateFraction(string $decimal) {
$fraction = Fraction::createFromDecimal($decimal);
$numerator = $fraction->getNumerator();
$denominator = $fraction->getDenominator();
// Set the numerator and denominator values for the field and store
// them in array which will be saved on the field.
return [
'decimal' => $decimal,
'numerator' => $numerator,
'denominator' => $denominator,
];
}
/**
* Helper method to validate.
*
* @param \Drupal\Core\Entity\EntityConstraintViolationList $violations
* Violation list.
*/
protected function assertViolations(EntityConstraintViolationList $violations) {
foreach ($violations as $violation) {
$constraint = $violation->getConstraint();
if (!($constraint instanceof FractionConstraint || $constraint instanceof PrimitiveTypeConstraint)) {
$this->fail('Wrong type of constraint');
}
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Drupal\Tests\fraction\Unit\Feeds\Target;
use Drupal\Core\Form\FormState;
use Drupal\fraction\Feeds\Target\FractionTarget;
use Drupal\Tests\feeds\Unit\Feeds\Target\FieldTargetTestBase;
/**
* @coversDefaultClass \Drupal\fraction\Feeds\Target\FractionTarget
* @group feeds
*/
class FractionTargetTest extends FieldTargetTestBase {
/**
* The ID of the plugin.
*
* @var string
*/
protected static $pluginId = 'fraction';
/**
* {@inheritdoc}
*/
protected function getTargetClass() {
return FractionTarget::class;
}
/**
* @covers ::prepareValue
*/
public function testPrepareValueFraction() {
$method = $this->getMethod('Drupal\fraction\Feeds\Target\FractionTarget', 'prepareTarget')->getClosure();
$configuration = [
'feed_type' => $this->createMock('Drupal\feeds\FeedTypeInterface'),
'target_definition' => $method($this->getMockFieldDefinition()),
'type' => 'fraction',
];
$target = new FractionTarget($configuration, 'fraction', []);
$method = $this->getProtectedClosure($target, 'prepareValue');
// Test a basic fraction: 1/2.
$values = ['value' => '1/2'];
$method(0, $values);
$this->assertSame($values['numerator'], '1');
$this->assertSame($values['denominator'], '2');
// Test that a zero numerator results in an empty fraction.
$values = ['value' => '0/1'];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
// Test that a zero denominator results in an empty fraction.
$values = ['value' => '1/0'];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
// Test that a negative numerator works.
$values = ['value' => '-1/2'];
$method(0, $values);
$this->assertSame($values['numerator'], '-1');
$this->assertSame($values['denominator'], '2');
// Test that a negative denominator results in an empty fraction.
$values = ['value' => '1/-2'];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
// Test that a decimal value results in an empty fraction.
$values = ['value' => '1.0'];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
// Test that a non-numeric value results in an empty fraction.
$values = ['value' => 'te/st'];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
}
/**
* @covers ::prepareValue
*/
public function testPrepareValueDecimal() {
$method = $this->getMethod('Drupal\fraction\Feeds\Target\FractionTarget', 'prepareTarget')->getClosure();
$configuration = [
'feed_type' => $this->createMock('Drupal\feeds\FeedTypeInterface'),
'target_definition' => $method($this->getMockFieldDefinition()),
'type' => 'decimal',
];
$target = new FractionTarget($configuration, 'fraction', []);
$method = $this->getProtectedClosure($target, 'prepareValue');
// Test a basic decimal.
$values = ['value' => 0.5];
$method(0, $values);
$this->assertSame($values['numerator'], '5');
$this->assertSame($values['denominator'], '10');
// Test a negative decimal.
$values = ['value' => -1];
$method(0, $values);
$this->assertSame($values['numerator'], '-1');
$this->assertSame($values['denominator'], '1');
// Test that a zero value results in 0/1.
$values = ['value' => 0];
$method(0, $values);
$this->assertSame($values['numerator'], '0');
$this->assertSame($values['denominator'], '1');
// Test that an empty value results in an empty fraction.
$values = ['value' => ''];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
// Test that a string value results in an empty fraction.
$values = ['value' => 'test'];
$method(0, $values);
$this->assertSame($values['numerator'], '');
$this->assertSame($values['denominator'], '');
}
/**
* @covers ::buildConfigurationForm
*/
public function testBuildConfigurationForm() {
$method = $this->getMethod('Drupal\fraction\Feeds\Target\FractionTarget', 'prepareTarget')->getClosure();
$configuration = [
'feed_type' => $this->createMock('Drupal\feeds\FeedTypeInterface'),
'target_definition' => $method($this->getMockFieldDefinition()),
];
$target = new FractionTarget($configuration, 'fraction', []);
$target->setStringTranslation($this->getStringTranslationStub());
$form_state = new FormState();
$form = $target->buildConfigurationForm([], $form_state);
$this->assertSame(count($form), 1);
$this->assertArrayHasKey('type', $form);
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Drupal\Tests\fraction\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\fraction\Fraction;
/**
* Provides tests for the Fraction class.
*
* @group Fraction
*/
class FractionTest extends UnitTestCase {
/**
* Returns a Fraction object from a numerator and denominator.
*
* This allows for easier chaining of Fraction methods in tests.
*
* @param int $numerator
* The fraction's numerator.
* @param int $denominator
* The fraction's denominator.
*
* @return \Drupal\fraction\Fraction
* Returns a $this->fraction object.
*/
protected function fraction($numerator = 0, $denominator = 1) {
return new Fraction($numerator, $denominator);
}
/**
* Test the Fraction class and it's methods.
*/
public function testFraction() {
// Test creation of a fraction.
$fraction = $this->fraction(1, 2);
$numerator = $fraction->getNumerator();
$denominator = $fraction->getDenominator();
$message = 'A fraction of 1/2 should return a numerator of 1.';
$this->assertEquals($numerator, '1', $message);
$message = 'A fraction of 1/2 should return a denominator of 2.';
$this->assertEquals($denominator, '2', $message);
// Test creation of an empty fraction.
$fraction = $this->fraction();
$numerator = $fraction->getNumerator();
$denominator = $fraction->getDenominator();
$message = 'An empty fraction should return a numerator of 0.';
$this->assertEquals($numerator, '0', $message);
$message = 'An empty fraction should return a denominator of 1.';
$this->assertEquals($denominator, '1', $message);
// Test returning a fraction as a string.
$result = $this->fraction(1, 2)->toString();
$message = 'A fraction with a numerator of 1 and a denominator of 2 should return a string of "1/2".';
$this->assertEquals($result, '1/2', $message);
// Test returning a fraction as a string with a different separator.
$result = $this->fraction(1, 2)->toString(':');
$message = 'A fraction with a numerator of 1 and a denominator of 2 should return a string of "1:2" (when a colon separator is specified).';
$this->assertEquals($result, '1:2', $message);
// Test division by zero.
$result = $this->fraction(1, 0)->toString();
$message = 'A fraction of 1/0 should return 0/1 (avoid division by zero).';
$this->assertEquals($result, '0/1', $message);
// Test normalization of negative fractions.
$result = $this->fraction(-1, 2)->toString();
$message = 'A fraction of -1/2 should normalize to -1/2.';
$this->assertEquals($result, '-1/2', $message);
$result = $this->fraction(1, -2)->toString();
$message = 'A fraction of 1/-2 should normalize to -1/2.';
$this->assertEquals($result, '-1/2', $message);
$result = $this->fraction(-1, -2)->toString();
$message = 'A fraction of -1/-2 should normalize to 1/2.';
$this->assertEquals($result, '1/2', $message);
// Test converting a fraction to a decimal.
$result = $this->fraction(1, 2)->toDecimal(1);
$message = 'A fraction of 1/2 should return a decimal of 0.5 (with precision 1)';
$this->assertEquals($result, '0.5', $message);
// Test decimal precision (rounding up).
$result = $this->fraction(1, 2)->toDecimal(0);
$message = 'A fraction of 1/2 with no precision should round to 1.';
$this->assertEquals($result, '1', $message);
// Test decimal precision (rounding down).
$result = $this->fraction(2, 5)->toDecimal(0);
$message = 'A fraction of 2/5 with no precision should round to 0.';
$this->assertEquals($result, '0', $message);
// Test automatic precision for base-10 denominator.
$result = $this->fraction(1, 1000)->toDecimal(2, TRUE);
$message = 'A fraction of 1/1000 with precision 2 and auto-precision enabled should round to 0.001.';
$this->assertEquals($result, '0.001', $message);
// Test automatic precision for denominator of 1.
$result = $this->fraction(3, 1)->toDecimal(0, TRUE);
$message = 'A fraction of 3/1 should return a decimal of 3 (with precision 0, auto_precision)';
$this->assertEquals($result, '3', $message);
// Test automatic precision for terminating fractions.
$result = $this->fraction(1, 8)->toDecimal(0, TRUE);
$message = 'A fraction of 1/8 should return a decimal of 0.125 (with precision 0, auto_precision)';
$this->assertEquals($result, '0.125', $message);
// Test automatic precision for fractions with a denominator greater than
// 2147483647.
$result = $this->fraction(1, 2147483648)->toDecimal(0, TRUE);
$message = 'A fraction of 1/2147483648 should return a decimal of 0.0000000004656612873077392578125 (with precision 0, auto_precision)';
$this->assertEquals('0.0000000004656612873077392578125', $result, $message);
// Test automatic precision for non-terminating fractions: 1/6.
$result = $this->fraction(1, 6)->toDecimal(3, TRUE);
$message = 'A fraction of 1/6 should return a decimal of 0.167 (with precision 0, auto_precision)';
$this->assertEquals($result, '0.167', $message);
// Test automatic precision for non-terminating fractions: 1/3.
$result = $this->fraction(1, 3)->toDecimal(3, TRUE);
$message = 'A fraction of 1/3 should return a decimal of 0.333 (with precision 3, auto_precision)';
$this->assertEquals($result, '0.333', $message);
// Test creation of a fraction from a decimal.
$result = Fraction::createFromDecimal(0.5)->toString();
$message = 'The createFromDecimal() method should create a fraction of 5/10 from a decimal of 0.5.';
$this->assertEquals($result, '5/10', $message);
// Test calculation of greatest common divisor.
$result = $this->fraction(5, 10)->gcd();
$message = 'The greatest common divisor of 1/2 is 5.';
$this->assertEquals($result, '5', $message);
// Test reduction of a fraction to its simplest form.
$result = $this->fraction(5, 10)->reduce()->toString();
$message = 'A fraction of 5/10 should be reducible to 1/2.';
$this->assertEquals($result, '1/2', $message);
// Test fraction addition.
$result = $this->fraction(1, 2)->add($this->fraction(1, 3))->toString();
$message = '1/2 + 1/3 = 5/6';
$this->assertEquals($result, '5/6', $message);
// Test fraction subtraction.
$result = $this->fraction(2, 3)->subtract($this->fraction(1, 7))->toString();
$message = '2/3 - 1/7 = 11/21';
$this->assertEquals($result, '11/21', $message);
// Test fraction multiplication.
$result = $this->fraction(2, 5)->multiply($this->fraction(1, 4))->toString();
$message = '2/5 * 1/4 = 1/10';
$this->assertEquals($result, '1/10', $message);
// Test fraction division.
$result = $this->fraction(5, 7)->divide($this->fraction(1, 5))->toString();
$message = '5/7 / 1/5 = 25/7';
$this->assertEquals($result, '25/7', $message);
// Test fraction reciprocation.
$result = $this->fraction(1, 2)->reciprocate()->toString();
$message = 'The reciprocal of 1/2 is 2/1.';
$this->assertEquals($result, '2/1', $message);
// Test that reciprocation of a zero numerator does not result in division
// by zero.
$result = $this->fraction(0, 1)->reciprocate()->toString();
$message = 'The reciprocal of 0/1 is 0/1 (avoid division by zero).';
$this->assertEquals($result, '0/1', $message);
// Test that decimal arithmetic results are reduced.
$result = Fraction::createFromDecimal('0.1')->add(Fraction::createFromDecimal('0.2'))->toDecimal(0, TRUE);
$message = '0.1 + 0.2 = 0.3';
$this->assertEquals('0.3', $result, $message);
// Test floating point arithmetic.
$result = Fraction::createFromDecimal('3')->subtract(Fraction::createFromDecimal('2.99'))->toDecimal(0, TRUE);
$message = '3 - 2.99 = 0.01';
$this->assertEquals('0.01', $result, $message);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\Tests\fraction\Unit\process;
use Drupal\Tests\migrate\Unit\process\MigrateProcessTestCase;
use Drupal\fraction\Plugin\migrate\process\DecimalFraction;
/**
* Tests the decimal fraction process plugin.
*
* @group migrate
* @coversDefaultClass \Drupal\fraction\Plugin\migrate\process\DecimalFraction
*/
class DecimalFractionTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->plugin = new DecimalFraction([], 'decimal_fraction', []);
parent::setUp();
}
/**
* Test decimal fraction plugin.
*
* @dataProvider decimalFractionDataProvider
*/
public function testDecimalFraction($input, $expected_output) {
$output = $this->plugin->transform($input, $this->migrateExecutable, $this->row, 'destinationproperty');
$this->assertSame($output['numerator'], $expected_output['numerator']);
$this->assertSame($output['denominator'], $expected_output['denominator']);
}
/**
* Data provider for testDecimalFraction().
*
* @return array
* Array of input values and expected output values.
*/
public function decimalFractionDataProvider() {
return [
'basic decimal' => [
'input' => 0.5,
'expected_output' => [
'numerator' => '5',
'denominator' => '10',
],
],
'negative decimal' => [
'input' => -1,
'expected_output' => [
'numerator' => '-1',
'denominator' => '1',
],
],
'zero value' => [
'input' => 0,
'expected_output' => [
'numerator' => '0',
'denominator' => '1',
],
],
'empty string' => [
'input' => '',
'expected_output' => [
'numerator' => '',
'denominator' => '',
],
],
'string' => [
'input' => 'test',
'expected_output' => [
'numerator' => '',
'denominator' => '',
],
],
];
}
}