diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 996ec1e09b3..39890289309 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -169,6 +169,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): conn.create_function("regexp", 2, _sqlite_regexp) conn.create_function("django_format_dtdelta", 3, _sqlite_format_dtdelta) conn.create_function("django_power", 2, _sqlite_power) + conn.create_function('LPAD', 3, _sqlite_lpad) + conn.create_function('RPAD', 3, _sqlite_rpad) conn.execute('PRAGMA foreign_keys = ON') return conn @@ -467,5 +469,15 @@ def _sqlite_regexp(re_pattern, re_string): return bool(re.search(re_pattern, str(re_string))) if re_string is not None else False +def _sqlite_lpad(text, length, fill_text): + if len(text) >= length: + return text[:length] + return (fill_text * length)[:length - len(text)] + text + + +def _sqlite_rpad(text, length, fill_text): + return (text + fill_text * length)[:length] + + def _sqlite_power(x, y): return x ** y diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index 5528be8c485..a0f5a9e8b24 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -6,8 +6,8 @@ from .datetime import ( TruncQuarter, TruncSecond, TruncTime, TruncWeek, TruncYear, ) from .text import ( - Chr, Concat, ConcatPair, Left, Length, Lower, LTrim, Ord, Replace, Right, - RTrim, StrIndex, Substr, Trim, Upper, + Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord, Replace, + Right, RPad, RTrim, StrIndex, Substr, Trim, Upper, ) from .window import ( CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile, @@ -24,8 +24,9 @@ __all__ = [ 'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncWeek', 'TruncYear', # text - 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LTrim', 'Ord', - 'Replace', 'Right', 'RTrim', 'StrIndex', 'Substr', 'Trim', 'Upper', + 'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim', + 'Ord', 'Replace', 'Right', 'RPad', 'RTrim', 'StrIndex', 'Substr', 'Trim', + 'Upper', # window 'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead', 'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber', diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py index 614522017f7..c57545bc6a2 100644 --- a/django/db/models/functions/text.py +++ b/django/db/models/functions/text.py @@ -110,6 +110,15 @@ class Lower(Transform): lookup_name = 'lower' +class LPad(Func): + function = 'LPAD' + + def __init__(self, expression, length, fill_text=Value(' '), **extra): + if not hasattr(length, 'resolve_expression') and length < 0: + raise ValueError("'length' must be greater or equal to 0.") + super().__init__(expression, length, fill_text, **extra) + + class LTrim(Transform): function = 'LTRIM' lookup_name = 'ltrim' @@ -141,6 +150,10 @@ class Right(Left): return Substr(self.source_expressions[0], self.source_expressions[1] * Value(-1)) +class RPad(LPad): + function = 'RPAD' + + class RTrim(Transform): function = 'RTRIM' lookup_name = 'rtrim' diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index 9208a905744..2d0d5d2fe74 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -800,6 +800,27 @@ Usage example:: >>> print(author.name_lower) margaret smith +``LPad`` +-------- + +.. class:: LPad(expression, length, fill_text=Value(' '), **extra) + +.. versionadded:: 2.1 + +Returns the value of the given text field or expression padded on the left side +with ``fill_text`` so that the resulting value is ``length`` characters long. +The default ``fill_text`` is a space. + +Usage example:: + + >>> from django.db.models import Value + >>> from django.db.models.functions import LPad + >>> Author.objects.create(name='John', alias='j') + >>> Author.objects.update(name=LPad('name', 8, Value('abc'))) + 1 + >>> print(Author.objects.get(alias='j').name) + abcaJohn + ``LTrim`` --------- @@ -872,6 +893,16 @@ Usage example:: >>> print(author.last_letter) h +``RPad`` +-------- + +.. class:: RPad(expression, length, fill_text=Value(' '), **extra) + +.. versionadded:: 2.1 + +Similar to :class:`~django.db.models.functions.LPad`, but pads on the right +side. + ``RTrim`` --------- diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index 35261b28501..4d8a9a1d422 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -205,10 +205,12 @@ Models * A number of new text database functions are added: :class:`~django.db.models.functions.Chr`, :class:`~django.db.models.functions.Left`, + :class:`~django.db.models.functions.LPad`, :class:`~django.db.models.functions.LTrim`, :class:`~django.db.models.functions.Ord`, :class:`~django.db.models.functions.Replace`, :class:`~django.db.models.functions.Right`, + :class:`~django.db.models.functions.RPad`, :class:`~django.db.models.functions.RTrim`, and :class:`~django.db.models.functions.Trim`. diff --git a/tests/db_functions/test_pad.py b/tests/db_functions/test_pad.py new file mode 100644 index 00000000000..d873345fc4d --- /dev/null +++ b/tests/db_functions/test_pad.py @@ -0,0 +1,34 @@ +from django.db.models import Value +from django.db.models.functions import LPad, RPad +from django.test import TestCase + +from .models import Author + + +class PadTests(TestCase): + def test_pad(self): + Author.objects.create(name='John', alias='j') + tests = ( + (LPad('name', 7, Value('xy')), 'xyxJohn'), + (RPad('name', 7, Value('xy')), 'Johnxyx'), + (LPad('name', 6, Value('x')), 'xxJohn'), + (RPad('name', 6, Value('x')), 'Johnxx'), + # The default pad string is a space. + (LPad('name', 6), ' John'), + (RPad('name', 6), 'John '), + # If string is longer than length it is truncated. + (LPad('name', 2), 'Jo'), + (RPad('name', 2), 'Jo'), + (LPad('name', 0), ''), + (RPad('name', 0), ''), + ) + for function, padded_name in tests: + with self.subTest(function=function): + authors = Author.objects.annotate(padded_name=function) + self.assertQuerysetEqual(authors, [padded_name], lambda a: a.padded_name, ordered=False) + + def test_pad_negative_length(self): + for function in (LPad, RPad): + with self.subTest(function=function): + with self.assertRaisesMessage(ValueError, "'length' must be greater or equal to 0."): + function('name', -1)