Browse Source

Refs #28643 -- Added LPad and RPad database functions.

Thanks Tim Graham for the review.
Mariusz Felisiak 7 years ago
parent
commit
cede5111bb

+ 12 - 0
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

+ 5 - 4
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',

+ 13 - 0
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'

+ 31 - 0
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``
 ---------
 

+ 2 - 0
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`.
 

+ 34 - 0
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)