Repository: bodo-ai/PyDough
Pull request #288: Implemented SIGN, FIND, STRIP Function
Author: login: vineetg3
Created: 2025-03-10T03:23:27Z | Merged: 2025-03-11T01:52:17Z

Resolves #225 .
Resolves #224.
Resolves #223
Please check the issues for more details.

The PRs introduces functionalities for:

**SIGN function to PyDough with the following functionality:**

- SIGN(X) returns:
  - 0 if X is equal to 0
  - 1 if X is greater than 0
  - -1 if X is less than 0
 
- Equivalent to Python's `0 if X == 0 else (1 if X > 0 else -1)`
- Equivalent to SQL's `CASE WHEN X = 0 THEN 0 WHEN X > 0 THEN 1 WHEN X < 0 THEN -1 END`

**FIND function to PyDough with the following functionality:**

- FIND(X, Y) returns the index of the first occurrence of substring Y in string X
- It is 0-indexed, returning -1 if the substring is not found
- Equivalent to Python's `X.find(Y)` or SQL's `POSITION(Y in X) - 1`


**STRIP function to the PyDough with the following functionality** 

It provides functionality equivalent to Python's `str.strip()` method or SQL's `TRIM`.  
- `STRIP(X)`: Removes leading and trailing whitespace (spaces, tabs, newlines) from `X`.  
- `STRIP(X, Y)`: Removes all leading and trailing characters in the set `Y` from `X`.  
This function is case-sensitive and continues trimming until a character not in the set is encountered.

The functions have been implemented and thoroughly tested with end-to-end tests in `test_pipeline.py`.

Linked issue #225: Add SIGN function in PyDough
Goal: add a `SIGN` function to PyDough with the following functionality, which should be tested e2e (see `test_pipeline.py` for examples):

- `SIGN(X)` is equivalent to the python string expression `0 if X == 0 else (1 if X > 0 else -1)`, or the SQL expression `CASE WHEN X == 0 THEN 0 WHEN X > 0 THEN 1 WHEN X < 0 THEN -1 END`

This feature need to be documented in the [function list documentation](https://github.com/bodo-ai/PyDough/blob/main/documentation/functions.md)

Diff:
diff --git a/demos/notebooks/5_what_if.ipynb b/demos/notebooks/5_what_if.ipynb
index 158117851..fd9305bb7 100644
--- a/demos/notebooks/5_what_if.ipynb
+++ b/demos/notebooks/5_what_if.ipynb
@@ -113,7 +113,7 @@
     "\n",
     "```python\n",
     "revenue_def = extended_price*(1-discount)\n",
-    "orders(total_line_price=SUM(lines(line_price=revenue_def).line_price)).lines(\n",
+    "orders.CALCULATE(total_line_price=SUM(lines.CALCULATE(line_price=revenue_def).line_price)).lines.CALCULATE(\n",
     "    revenue_ratio=revenue_def / total_line_price, \n",
     "    order_key=order_key, \n",
     "    line_number=line_number\n",
@@ -200,7 +200,7 @@
    "source": [
     "%%pydough\n",
     "\n",
-    "total_revenue = SUM(lines.CALCULATE(line_renvenue=revenue_def).line_renvenue)\n",
+    "total_revenue = SUM(lines.CALCULATE(line_revenue=revenue_def).line_revenue)\n",
     "total_revenue"
    ]
   },
@@ -296,7 +296,7 @@
    "id": "12ca1882-91f3-4da2-aa01-94212a67c750",
    "metadata": {},
    "source": [
-    "Now we will build our final question by moving from the context of the orders to the context of orders and lines together. Since Orders -> Lines is a Many to One relationship, this places us in the context of lines with some additional information.\n",
+    "Now we will build our final question by moving from the context of the orders to the context of orders and lines together. Since Orders -> Lines is a One to Many relationship, this places us in the context of lines with some additional information.\n",
     "\n",
     "For actually fully solving our prior question, we will compute the ratio and then select 5 smallest ratio value, breaking ties with a combination of the order number and line number."
    ]
diff --git a/documentation/functions.md b/documentation/functions.md
index 58771d8c4..a0fe819cb 100644
--- a/documentation/functions.md
+++ b/documentation/functions.md
@@ -23,6 +23,8 @@ Below is the list of every function/operator currently supported in PyDough as a
    * [JOIN_STRINGS](#join_strings)
    * [LPAD](#lpad)
    * [RPAD](#rpad)
+   * [FIND](#find)
+   * [STRIP](#strip)
 - [Datetime Functions](#datetime-functions)
    * [DATETIME](#datetime)
    * [YEAR](#year)
@@ -45,6 +47,7 @@ Below is the list of every function/operator currently supported in PyDough as a
    * [ROUND](#round)
    * [POWER](#power)
    * [SQRT](#sqrt)
+   * [SIGN](#sign)
 - [Aggregation Functions](#aggregation-functions)
    * [SUM](#sum)
    * [AVG](#avg)
@@ -332,6 +335,47 @@ Here are examples on how it pads on string literals:
 | `RPAD("123", 2, "0")` | `"12"` |
 | `RPAD("123", 0, "0")` | `""` |
 
+<!-- TOC --><a name="find"></a>
+
+### FIND
+
+The `FIND` function returns the position (0-indexed) of the first occurrence of a substring within a string, or -1 if the substring is not found. The first argument is the string to search within, and the second argument is the substring to search for.
+
+```py
+Customers.WHERE(name == "Alex Rodriguez")
+         .CALCULATE(
+            idx_Alex = FIND(name, "Alex"), # 0
+            idx_Rodriguez = FIND(name, "Rodriguez"), # 5
+            idx_bob = FIND(name, "bob"), # -1
+            idx_e = FIND(name, "e"), # 2
+            idx_space = FIND(name, " "), # 4
+            idx_of_R = FIND(name, "R"), # 5
+            idx_of_Alex_Rodriguez = FIND(name, "Alex Rodriguez"), # 0
+)
+```
+
+<!-- TOC --><a name="strip"></a>
+
+### STRIP
+
+The `STRIP` function returns the first argument with all leading and trailing whitespace removed, including newlines, tabs, and spaces.
+If the second argument is provided, it is used as the set of characters to remove from the leading and trailing ends of the first argument.
+It continues removing characters until it encounters a character that is not in the set.
+This function is equivalent to python's `str.strip()` method.
+Note: This function is case-sensitive.
+
+```py
+Customers.CALCULATE(stripped_name = STRIP(name)) # removes all leading and trailing whitespace
+Customers.CALCULATE(stripped_name = STRIP(name, "aeiou")) # removes all leading and trailing vowels
+```
+
+| **Input String (X)**       | **STRIP(X, Y)**                    | **Result**          |
+|-----------------------------|---------------------------------------|---------------------|
+| `'abcXYZcba'`              | `STRIP('abcXYZcba','abc')`        | `'XYZ'`            |
+| `'$$Hello$$'`              | `STRIP('$$Hello$$','$$')`          | `'Hello'`          |
+| `'---Test-String---'`      | `STRIP('---Test-String---','-')`  | `'Test-String'`    |
+| `'123456Hello654321'`      | `STRIP('123456Hello654321','123456')` | `'Hello'`         |
+
 <!-- TOC --><a name="datetime-functions"></a>
 
 ## Datetime Functions
@@ -610,6 +654,16 @@ The `SQRT` function takes the square root of its input. It's equivalent to `POWE
 Parts.CALCULATE(sqrt_price = SQRT(retail_price))
 ```
 
+<!-- TOC --><a name="sign"></a>
+
+### SIGN
+
+The `SIGN` function returns the sign of its input. It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
+
+```py
+Suppliers.CALCULATE(sign_of_acctbal = SIGN(account_balance))
+```
+
 <!-- TOC --><a name="aggregation-functions"></a>
 
 ## Aggregation Functions
diff --git a/pydough/pydough_operators/__init__.py b/pydough/pydough_operators/__init__.py
index 9676b59fd..9b2dbc1dc 100644
--- a/pydough/pydough_operators/__init__.py
+++ b/pydough/pydough_operators/__init__.py
@@ -27,6 +27,7 @@
     "ExpressionFunctionOperator",
     "ExpressionTypeDeducer",
     "ExpressionWindowOperator",
+    "FIND",
     "GEQ",
     "GRT",
     "HAS",
@@ -63,9 +64,11 @@
     "RPAD",
     "RequireNumArgs",
     "SECOND",
+    "SIGN",
     "SLICE",
     "SQRT",
     "STARTSWITH",
+    "STRIP",
     "SUB",
     "SUM",
     "SelectArgumentType",
@@ -94,6 +97,7 @@
     DIV,
     ENDSWITH,
     EQU,
+    FIND,
     GEQ,
     GRT,
     HAS,
@@ -127,9 +131,11 @@
     ROUND,
     RPAD,
     SECOND,
+    SIGN,
     SLICE,
     SQRT,
     STARTSWITH,
+    STRIP,
     SUB,
     SUM,
     UPPER,
diff --git a/pydough/pydough_operators/expression_operators/README.md b/pydough/pydough_operators/expression_operators/README.md
index 85105065f..05d0b724a 100644
--- a/pydough/pydough_operators/expression_operators/README.md
+++ b/pydough/pydough_operators/expression_operators/README.md
@@ -77,7 +77,8 @@ These functions must be called on singular data as a function.
 - `JOIN_STRINGS`: equivalent to the Python string join method, where the first argument is used as a delimiter to concatenate the remaining arguments.
 - `LPAD`: pads the first argument with the second argument to the left until the first argument is equal in length to the third argument.
 - `RPAD`: pads the first argument with the second argument to the right until the first argument is equal in length to the third argument.
-
+- `FIND`: returns the index(0-indexed) of the first occurrence of the second argument within the first argument, or -1 if the second argument is not found.
+- `STRIP`: returns the first argument with all leading and trailing whitespace removed, including newlines, tabs, and spaces. If the second argument is provided, it is used as the set of characters to remove from the leading and trailing ends of the first argument.
 ##### Datetime Functions
 
 - `DATETIME`: constructs a new datetime, either from an existing one or the current datetime, and augments it by adding/subtracting intervals of time and/or truncating it to various units.
@@ -110,6 +111,7 @@ These functions must be called on singular data as a function.
 - `ROUND`: rounds the first argument to a number of digits equal to the second argument. If second argument is not provided, the first argument is rounded to 0 decimal places.
 - `POWER`: exponentiates the first argument to the power of second argument.
 - `SQRT`: returns the square root of the input. 
+- `SIGN`: returns the sign of the input. It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
 
 #### Aggregation Functions
 
diff --git a/pydough/pydough_operators/expression_operators/__init__.py b/pydough/pydough_operators/expression_operators/__init__.py
index b333a59a5..72002fc6d 100644
--- a/pydough/pydough_operators/expression_operators/__init__.py
+++ b/pydough/pydough_operators/expression_operators/__init__.py
@@ -24,6 +24,7 @@
     "EQU",
     "ExpressionFunctionOperator",
     "ExpressionWindowOperator",
+    "FIND",
     "GEQ",
     "GRT",
     "HAS",
@@ -58,9 +59,11 @@
     "ROUND",
     "RPAD",
     "SECOND",
+    "SIGN",
     "SLICE",
     "SQRT",
     "STARTSWITH",
+    "STRIP",
     "SUB",
     "SUM",
     "UPPER",
@@ -88,6 +91,7 @@
     DIV,
     ENDSWITH,
     EQU,
+    FIND,
     GEQ,
     GRT,
     HAS,
@@ -121,9 +125,11 @@
     ROUND,
     RPAD,
     SECOND,
+    SIGN,
     SLICE,
     SQRT,
     STARTSWITH,
+    STRIP,
     SUB,
     SUM,
     UPPER,
diff --git a/pydough/pydough_operators/expression_operators/registered_expression_operators.py b/pydough/pydough_operators/expression_operators/registered_expression_operators.py
index b6d54aab8..8f797ec92 100644
--- a/pydough/pydough_operators/expression_operators/registered_expression_operators.py
+++ b/pydough/pydough_operators/expression_operators/registered_expression_operators.py
@@ -19,6 +19,7 @@
     "DIV",
     "ENDSWITH",
     "EQU",
+    "FIND",
     "GEQ",
     "GRT",
     "HAS",
@@ -52,9 +53,11 @@
     "ROUND",
     "RPAD",
     "SECOND",
+    "SIGN",
     "SLICE",
     "SQRT",
     "STARTSWITH",
+    "STRIP",
     "SUB",
     "SUM",
     "UPPER",
@@ -106,6 +109,9 @@
 STARTSWITH = ExpressionFunctionOperator(
     "STARTSWITH", False, RequireNumArgs(2), ConstantType(BooleanType())
 )
+STRIP = ExpressionFunctionOperator(
+    "STRIP", False, RequireArgRange(1, 2), SelectArgumentType(0)
+)
 ENDSWITH = ExpressionFunctionOperator(
     "ENDSWITH", False, RequireNumArgs(2), ConstantType(BooleanType())
 )
@@ -125,6 +131,9 @@
 SQRT = ExpressionFunctionOperator(
     "SQRT", False, RequireNumArgs(1), ConstantType(Float64Type())
 )
+SIGN = ExpressionFunctionOperator(
+    "SIGN", False, RequireNumArgs(1), ConstantType(Int64Type())
+)
 COUNT = ExpressionFunctionOperator("COUNT", True, AllowAny(), ConstantType(Int64Type()))
 HAS = ExpressionFunctionOperator("HAS", True, AllowAny(), ConstantType(BooleanType()))
 HASNOT = ExpressionFunctionOperator(
@@ -169,6 +178,9 @@
 RPAD = ExpressionFunctionOperator(
     "RPAD", False, RequireNumArgs(3), SelectArgumentType(0)
 )
+FIND = ExpressionFunctionOperator(
+    "FIND", False, RequireNumArgs(2), ConstantType(Int64Type())
+)
 NOT = ExpressionFunctionOperator(
     "NOT", False, RequireNumArgs(1), ConstantType(BooleanType())
 )
diff --git a/pydough/pydough_operators/type_inference/README.md b/pydough/pydough_operators/type_inference/README.md
index 80d1bfc53..96b4977ed 100644
--- a/pydough/pydough_operators/type_inference/README.md
+++ b/pydough/pydough_operators/type_inference/README.md
@@ -17,6 +17,7 @@ This subdirectory of the PyDough operators directory deals with utilities used f
 - `AllowAny`: Type verifier implementation class that always accepts, no matter the arguments.
 - `RequireNumArgs`: Type verifier implementation class that requires an exact number of arguments.
 - `RequireMinArgs`: Type verifier implementation class that requires a minimum number of arguments.
+- `RequireArgRange`: Type verifier implementation class that requires the number of arguments to be between a minimum and maximum number inclusive on both ends.
 
 ## Usage
 
diff --git a/pydough/sqlglot/transform_bindings.py b/pydough/sqlglot/transform_bindings.py
index 6fe49d670..af453e89d 100644
--- a/pydough/sqlglot/transform_bindings.py
+++ b/pydough/sqlglot/transform_bindings.py
@@ -1538,6 +1538,106 @@ def convert_round(
     )
 
 
+def convert_sign(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for getting the sign of the operand.
+    It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+    Returns:
+        The SQLGlot expression matching the functionality of `SIGN(X)`.
+    """
+    assert len(sql_glot_args) == 1
+    arg: SQLGlotExpression = sql_glot_args[0]
+    zero_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(0)
+    one_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(1)
+    minus_one_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(-1)
+    answer: SQLGlotExpression = convert_iff_case(
+        None,
+        [
+            sqlglot_expressions.EQ(this=arg, expression=zero_glot),
+            zero_glot,
+            apply_parens(
+                convert_iff_case(
+                    None,
+                    [
+                        sqlglot_expressions.LT(this=arg, expression=zero_glot),
+                        minus_one_glot,
+                        one_glot,
+                    ],
+                ),
+            ),
+        ],
+    )
+    return answer
+
+
+def convert_strip(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for removing all leading and trailing whitespace from a string.
+    If a second argument is provided, it is used as the set of characters
+    to remove from the leading and trailing ends of the first argument.
+
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+
+    Returns:
+        The SQLGlot expression matching the functionality of `STRIP(X, Y)`.
+        In Python, this is equivalent to `X.strip(Y)`.
+    """
+    assert 1 <= len(sql_glot_args) <= 2
+    to_strip: SQLGlotExpression = sql_glot_args[0]
+    strip_char_glot: SQLGlotExpression
+    if len(sql_glot_args) == 1:
+        strip_char_glot = sqlglot_expressions.Literal.string("\n\t ")
+    else:
+        strip_char_glot = sql_glot_args[1]
+    return sqlglot_expressions.Trim(
+        this=to_strip,
+        expression=strip_char_glot,
+    )
+
+
+def convert_find(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for getting the index of the first occurrence of a substring within a string.
+    The first argument is the string to search within,
+    and the second argument is the substring to search for.
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+    Returns:
+        The SQLGlot expression matching the functionality of `FIND(this, expression)`,
+        i.e the index of the first occurrence of the second argument within
+        the first argument, or -1 if the second argument is not found.
+    """
+    assert len(sql_glot_args) == 2
+    answer: SQLGlotExpression = sqlglot_expressions.Sub(
+        this=sqlglot_expressions.StrPosition(
+            this=sql_glot_args[0], substr=sql_glot_args[1]
+        ),
+        expression=sqlglot_expressions.Literal.number(1),
+    )
+    return answer
+
+
 class SqlGlotTransformBindings:
     """
     Binding infrastructure used to associate PyDough operators with a procedure
@@ -1693,9 +1793,13 @@ def add_builtin_bindings(self) -> None:
         self.bindings[pydop.JOIN_STRINGS] = convert_concat_ws
         self.bindings[pydop.LPAD] = convert_lpad
         self.bindings[pydop.RPAD] = convert_rpad
+        self.bindings[pydop.FIND] = convert_find
+        self.bindings[pydop.STRIP] = convert_strip
 
         # Numeric functions
         self.bind_simple_function(pydop.ABS, sqlglot_expressions.Abs)
+        self.bind_simple_function(pydop.ROUND, sqlglot_expressions.Round)
+        self.bindings[pydop.SIGN] = convert_sign
         self.bindings[pydop.ROUND] = convert_round
 
         # Conditional functions
diff --git a/tests/simple_pydough_functions.py b/tests/simple_pydough_functions.py
index 61f60507e..a7dcee3f7 100644
--- a/tests/simple_pydough_functions.py
+++ b/tests/simple_pydough_functions.py
@@ -973,3 +973,59 @@ def step_slicing():
         wo_step8=name[-4:-2],
         wo_step9=name[2:2],
     )
+
+
+def sign():
+    return (
+        DailyPrices.CALCULATE(
+            high,
+            high_neg=-1 * high,
+            high_zero=0 * high,
+        )
+        .TOP_K(5, by=high.ASC())
+        .CALCULATE(
+            high,
+            high_neg,
+            high_zero,
+            sign_high=SIGN(high),
+            sign_high_neg=SIGN(high_neg),
+            sign_high_zero=SIGN(high_zero),
+        )
+    )
+
+
+def find():
+    return Customers.WHERE(name == "Alex Rodriguez").CALCULATE(
+        name,
+        idx_Alex=FIND(name, "Alex"),
+        idx_Rodriguez=FIND(name, "Rodriguez"),
+        idx_bob=FIND(name, "bob"),
+        idx_e=FIND(name, "e"),
+        idx_space=FIND(name, " "),
+        idx_of_R=FIND(name, "R"),
+        idx_of_Alex_Rodriguez=FIND(name, "Alex Rodriguez"),
+    )
+
+
+def strip():
+    return (
+        Customers.WHERE(name == "Alex Rodriguez")
+        .CALCULATE(
+            name,
+            alt_name1="  Alex Rodriguez  ",
+            alt_name2="aeiAlex Rodriguezaeiou",
+            alt_name3=";;Alex Rodriguez;;",
+            alt_name4="""
+    Alex Rodriguez
+        """,  # equivalent to "\n\tAlex Rodriguez\n"
+        )
+        .CALCULATE(
+            stripped_name=STRIP(name, "Alex Rodriguez"),
+            stripped_name1=STRIP(name),
+            stripped_name_with_chars=STRIP(name, "lAez"),
+            stripped_alt_name1=STRIP(alt_name1),
+            stripped_alt_name2=STRIP(alt_name2, "aeiou"),
+            stripped_alt_name3=STRIP(alt_name3, ";"),
+            stripped_alt_name4=STRIP(alt_name4),
+        )
+    )
diff --git a/tests/test_pipeline_defog_custom.py b/tests/test_pipeline_defog_custom.py
index 92926e57a..b0698c3fa 100644
--- a/tests/test_pipeline_defog_custom.py
+++ b/tests/test_pipeline_defog_custom.py
@@ -29,6 +29,7 @@
 )
 from simple_pydough_functions import (
     exponentiation,
+    find,
     hour_minute_day,
     minutes_seconds_datediff,
     multi_partition_access_1,
@@ -38,7 +39,9 @@
     multi_partition_access_5,
     multi_partition_access_6,
     padding_functions,
+    sign,
     step_slicing,
+    strip,
     years_months_days_hours_datediff,
 )
 from test_utils import (
@@ -595,8 +598,8 @@
                         ]
                     }
                 ).assign(
-                    ref_rpad=lambda x: "Cust0001**********************",
-                    ref_lpad=lambda x: "**********************Cust0001",
+                    ref_rpad="Cust0001**********************",
+                    ref_lpad="**********************Cust0001",
                     right_padded=lambda x: x.original_name.apply(
                         lambda s: (s + "*" * 30)[:30]
                     ),
@@ -709,6 +712,69 @@
             ),
             id="step_slicing",
         ),
+        pytest.param(
+            (
+                sign,
+                None,
+                "Broker",
+                "sign",
+                lambda: pd.DataFrame(
+                    {
+                        "high": [83.0, 83.6, 84.2, 84.8, 85.4],
+                    }
+                ).assign(
+                    high_neg=lambda x: x["high"] * -1,
+                    high_zero=lambda x: x["high"] * 0,
+                    sign_high=1,
+                    sign_high_neg=-1,
+                    sign_high_zero=0,
+                ),
+            ),
+            id="sign",
+        ),
+        pytest.param(
+            (
+                find,
+                None,
+                "Broker",
+                "find",
+                lambda: pd.DataFrame(
+                    {
+                        "name": ["Alex Rodriguez"],
+                        "idx_Alex": ["Alex Rodriguez".find("Alex")],
+                        "idx_Rodriguez": ["Alex Rodriguez".find("Rodriguez")],
+                        "idx_bob": ["Alex Rodriguez".find("bob")],
+                        "idx_e": ["Alex Rodriguez".find("e")],
+                        "idx_space": ["Alex Rodriguez".find(" ")],
+                        "idx_of_R": ["Alex Rodriguez".find("R")],
+                        "idx_of_Alex_Rodriguez": [
+                            "Alex Rodriguez".find("Alex Rodriguez")
+                        ],
+                    }
+                ),
+            ),
+            id="find",
+        ),
+        pytest.param(
+            (
+                strip,
+                None,
+                "Broker",
+                "strip",
+                lambda: pd.DataFrame(
+                    {
+                        "stripped_name": [""],
+                        "stripped_name1": ["Alex Rodriguez"],
+                        "stripped_name_with_chars": ["x Rodrigu"],
+                        "stripped_alt_name1": ["Alex Rodriguez"],
+                        "stripped_alt_name2": ["Alex Rodriguez"],
+                        "stripped_alt_name3": ["Alex Rodriguez"],
+                        "stripped_alt_name4": ["Alex Rodriguez"],
+                    }
+                ),
+            ),
+            id="strip",
+        ),
     ],
 )
 def custom_defog_test_data(
diff --git a/tests/test_plan_refsols/find.txt b/tests/test_plan_refsols/find.txt
new file mode 100644
index 000000000..fa344d009
--- /dev/null
+++ b/tests/test_plan_refsols/find.txt
@@ -0,0 +1,4 @@
+ROOT(columns=[('name', name), ('idx_Alex', idx_Alex), ('idx_Rodriguez', idx_Rodriguez), ('idx_bob', idx_bob), ('idx_e', idx_e), ('idx_space', idx_space), ('idx_of_R', idx_of_R), ('idx_of_Alex_Rodriguez', idx_of_Alex_Rodriguez)], orderings=[])
+ PROJECT(columns={'idx_Alex': FIND(name, 'Alex':string), 'idx_Rodriguez': FIND(name, 'Rodriguez':string), 'idx_bob': FIND(name, 'bob':string), 'idx_e': FIND(name, 'e':string), 'idx_of_Alex_Rodriguez': FIND(name, 'Alex Rodriguez':string), 'idx_of_R': FIND(name, 'R':string), 'idx_space': FIND(name, ' ':string), 'name': name})
+  FILTER(condition=name == 'Alex Rodriguez':string, columns={'name': name})
+   SCAN(table=main.sbCustomer, columns={'name': sbCustName})
diff --git a/tests/test_plan_refsols/sign.txt b/tests/test_plan_refsols/sign.txt
new file mode 100644
index 000000000..44f63f699
--- /dev/null
+++ b/tests/test_plan_refsols/sign.txt
@@ -0,0 +1,6 @@
+ROOT(columns=[('high', high), ('high_neg', high_neg), ('high_zero', high_zero), ('sign_high', sign_high), ('sign_high_neg', sign_high_neg), ('sign_high_zero', sign_high_zero)], orderings=[(ordering_0):asc_first])
+ PROJECT(columns={'high': high, 'high_neg': high_neg, 'high_zero': high_zero, 'ordering_0': ordering_0, 'sign_high': SIGN(high), 'sign_high_neg': SIGN(high_neg), 'sign_high_zero': SIGN(high_zero)})
+  LIMIT(limit=Literal(value=5, type=Int64Type()), columns={'high': high, 'high_neg': high_neg, 'high_zero': high_zero, 'ordering_0': ordering_0}, orderings=[(ordering_0):asc_first])
+   PROJECT(columns={'high': high, 'high_neg': high_neg, 'high_zero': high_zero, 'ordering_0': high})
+    PROJECT(columns={'high': high, 'high_neg': -1:int64 * high, 'high_zero': 0:int64 * high})
+     SCAN(table=main.sbDailyPrice, columns={'high': sbDpHigh})
diff --git a/tests/test_plan_refsols/strip.txt b/tests/test_plan_refsols/strip.txt
new file mode 100644
index 000000000..a07da3637
--- /dev/null
+++ b/tests/test_plan_refsols/strip.txt
@@ -0,0 +1,5 @@
+ROOT(columns=[('stripped_name', stripped_name), ('stripped_name1', stripped_name1), ('stripped_name_with_chars', stripped_name_with_chars), ('stripped_alt_name1', stripped_alt_name1), ('stripped_alt_name2', stripped_alt_name2), ('stripped_alt_name3', stripped_alt_name3), ('stripped_alt_name4', stripped_alt_name4)], orderings=[])
+ PROJECT(columns={'stripped_alt_name1': STRIP(alt_name1), 'stripped_alt_name2': STRIP(alt_name2, 'aeiou':string), 'stripped_alt_name3': STRIP(alt_name3, ';':string), 'stripped_alt_name4': STRIP(alt_name4), 'stripped_name': STRIP(name, 'Alex Rodriguez':string), 'stripped_name1': STRIP(name), 'stripped_name_with_chars': STRIP(name, 'lAez':string)})
+  PROJECT(columns={'alt_name1': '  Alex Rodriguez  ':string, 'alt_name2': 'aeiAlex Rodriguezaeiou':string, 'alt_name3': ';;Alex Rodriguez;;':string, 'alt_name4': '\n    Alex Rodriguez\n        ':string, 'name': name})
+   FILTER(condition=name == 'Alex Rodriguez':string, columns={'name': name})
+    SCAN(table=main.sbCustomer, columns={'name': sbCustName})
diff --git a/tests/tpch_test_functions.py b/tests/tpch_test_functions.py
index a5176ff86..62814aa13 100644
--- a/tests/tpch_test_functions.py
+++ b/tests/tpch_test_functions.py
@@ -1,3 +1,7 @@
+"""
+PyDough implementation of TPCH queries.
+"""
+
 __all__ = [
     "impl_tpch_q1",
     "impl_tpch_q10",


Reviews:
user: login: knassre-bodo
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:11:10Z
user: login: hadia206
body: Looks good to me.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T20:53:15Z

Review comments:
diff_hunk: @@ -332,6 +335,47 @@ Here are examples on how it pads on string literals:
 | `RPAD("123", 2, "0")` | `"12"` |
 | `RPAD("123", 0, "0")` | `""` |
 
+<!-- TOC --><a name="find"></a>
+
+### FIND
+
+The `FIND` function returns the index(0-indexed) of the first occurrence of the second argument within the first argument, or -1 if the second argument is not found.
path: documentation/functions.md
user: login: knassre-bodo
body: Missing space: `index (0-indexed)`. Also, I recommend changing the terminology to be about strings/substrings.
created_at: 2025-03-10T16:07:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1492,6 +1492,109 @@ def convert_sqlite_datediff(
             raise ValueError(f"Unsupported argument '{unit}' for DATEDIFF.")
 
 
+def convert_sign(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for getting the sign of the operand.
+    It returns 1 if the input is positive, -1 if the input is negative, and 0 if the input is zero.
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+    Returns:
+        The SQLGlot expression matching the functionality of `SIGN(X)`.
+    """
+    assert len(sql_glot_args) == 1
+    arg: SQLGlotExpression = sql_glot_args[0]
+    zero_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(0)
+    one_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(1)
+    minus_one_glot: SQLGlotExpression = sqlglot_expressions.Literal.number(-1)
+    answer: SQLGlotExpression = convert_iff_case(
+        None,
+        [
+            sqlglot_expressions.EQ(this=arg, expression=zero_glot),
+            zero_glot,
+            apply_parens(
+                convert_iff_case(
+                    None,
+                    [
+                        sqlglot_expressions.LT(this=arg, expression=zero_glot),
+                        minus_one_glot,
+                        one_glot,
+                    ],
+                ),
+            ),
+        ],
+    )
+    return answer
+
+
+def convert_strip(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for removing all leading and trailing whitespace from a string.
+    If a second argument is provided, it is used as the set of characters
+    to remove from the leading and trailing ends of the first argument.
+
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+
+    Returns:
+        The SQLGlot expression matching the functionality of `STRIP(X, Y)`.
+        In Python, this is equivalent to `X.strip(Y)`.
+    """
+    assert 1 <= len(sql_glot_args) <= 2
+    to_strip: SQLGlotExpression = sql_glot_args[0]
+    strip_char_glot: SQLGlotExpression
+    if len(sql_glot_args) == 1:
+        strip_char_glot = sqlglot_expressions.Literal.string("\n\t ")
+    else:
+        strip_char_glot = sql_glot_args[1]
+    return sqlglot_expressions.Trim(
+        this=to_strip,
+        expression=strip_char_glot,
+    )
+
+
+def convert_find(
+    raw_args: Sequence[RelationalExpression] | None,
+    sql_glot_args: Sequence[SQLGlotExpression],
+) -> SQLGlotExpression:
+    """
+    Support for getting the index of the first occurrence of a substring within a string.
+    The first argument is the string to search within,
+    and the second argument is the substring to search for.
+    Args:
+        `raw_args`: The operands passed to the function before they were converted to
+        SQLGlot expressions. (Not actively used in this implementation.)
+        `sql_glot_args`: The operands passed to the function after they were converted
+        to SQLGlot expressions.
+    Returns:
+        The SQLGlot expression matching the functionality of `FIND(this, expression)`,
+        i.e the index of the first occurrence of the second argument within
+        the first argument, or -1 if the second argument is not found.
+    """
+    assert len(sql_glot_args) == 2
+    # answer = sqlglot_expressions.StrPosition(
+    #         this=sql_glot_args[0], position=sql_glot_args[1]
+    #     )
path: pydough/sqlglot/transform_bindings.py
user: login: hadia206
body: remove
created_at: 2025-03-10T20:50:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: review-notebook-app[bot]
created_at: 2025-03-10T08:32:03Z
body: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/bodo-ai/PyDough/pull/288"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
author_association: NONE
user: login: vineetg3
created_at: 2025-03-10T08:33:50Z
body: PR #284 included in this PR.
author_association: CONTRIBUTOR