If column contains word

This should ideally be done with the help of SQL Server full text search if using that.

However, if you can’t get that working on your DB for some reason, here is a performance-intensive solution:

-- table to search in
CREATE TABLE dbo.myTable
    (
    myTableId int NOT NULL IDENTITY (1, 1),
    code varchar(200) NOT NULL,
    description varchar(200) NOT NULL -- this column contains the values we are going to search in
    )  ON [PRIMARY]
GO

-- function to split space separated search string into individual words
CREATE FUNCTION [dbo].[fnSplit] (@StringInput nvarchar(max),
@Delimiter nvarchar(1))
RETURNS @OutputTable TABLE (
  id nvarchar(1000)
)
AS
BEGIN
  DECLARE @String nvarchar(100);

  WHILE LEN(@StringInput) > 0
  BEGIN
    SET @String = LEFT(@StringInput, ISNULL(NULLIF(CHARINDEX(@Delimiter, @StringInput) - 1, -1),
    LEN(@StringInput)));
    SET @StringInput = SUBSTRING(@StringInput, ISNULL(NULLIF(CHARINDEX
    (
    @Delimiter, @StringInput
    ),
    0
    ), LEN
    (
    @StringInput)
    )
    + 1, LEN(@StringInput));

    INSERT INTO @OutputTable (id)
      VALUES (@String);
  END;

  RETURN;
END;
GO

-- this is the search script which can be optionally converted to a stored procedure /function


declare @search varchar(max) = 'infection upper acute genito'; -- enter your search string here
-- the searched string above should give rows containing the following
-- infection in upper side with acute genitointestinal tract
-- acute infection in upper teeth
-- acute genitointestinal pain

if (len(trim(@search)) = 0) -- if search string is empty, just return records ordered alphabetically
begin
 select 1 as Priority ,myTableid, code, Description from myTable order by Description
 return;
end

declare @splitTable Table(
wordRank int Identity(1,1), -- individual words are assinged priority order (in order of occurence/position)
word varchar(200)
)
declare @nonWordTable Table( -- table to trim out auxiliary verbs, prepositions etc. from the search
id varchar(200)
)

insert into @nonWordTable values
('of'),
('with'),
('at'),
('in'),
('for'),
('on'),
('by'),
('like'),
('up'),
('off'),
('near'),
('is'),
('are'),
(','),
(':'),
(';')

insert into @splitTable
select id from dbo.fnSplit(@search,' '); -- this function gives you a table with rows containing all the space separated words of the search like in this e.g., the output will be -
--  id
-------------
-- infection
-- upper
-- acute
-- genito

delete s from @splitTable s join @nonWordTable n  on s.word = n.id; -- trimming out non-words here
declare @countOfSearchStrings int = (select count(word) from @splitTable);  -- count of space separated words for search
declare @highestPriority int = POWER(@countOfSearchStrings,3);

with plainMatches as
(
select myTableid, @highestPriority as Priority from myTable where Description like @search  -- exact matches have highest priority
union
select myTableid, @highestPriority-1 as Priority from myTable where Description like  @search + '%'  -- then with something at the end
union
select myTableid, @highestPriority-2 as Priority from myTable where Description like '%' + @search -- then with something at the beginning
union
select myTableid, @highestPriority-3 as Priority from myTable where Description like '%' + @search + '%' -- then if the word falls somewhere in between
),
splitWordMatches as( -- give each searched word a rank based on its position in the searched string
                     -- and calculate its char index in the field to search
select myTable.myTableid, (@countOfSearchStrings - s.wordRank) as Priority, s.word,
wordIndex = CHARINDEX(s.word, myTable.Description)  from myTable join @splitTable s on myTable.Description like '%'+ s.word + '%'
-- and not exists(select myTableid from plainMatches p where p.myTableId = myTable.myTableId) -- need not look into myTables that have already been found in plainmatches as they are highest ranked
                                                                              -- this one takes a long time though, so commenting it, will have no impact on the result
),
matchingRowsWithAllWords as (
 select myTableid, count(myTableid) as myTableCount from splitWordMatches group by(myTableid) having count(myTableid) = @countOfSearchStrings
)
, -- trim off the CTE here if you don't care about the ordering of words to be considered for priority
wordIndexRatings as( -- reverse the char indexes retrived above so that words occuring earlier have higher weightage
                     -- and then normalize them to sequential values
select s.myTableid, Priority, word, ROW_NUMBER() over (partition by s.myTableid order by wordindex desc) as comparativeWordIndex
from splitWordMatches s join matchingRowsWithAllWords m on s.myTableId = m.myTableId
)
,
wordIndexSequenceRatings as ( -- need to do this to ensure that if the same set of words from search string is found in two rows,
                              -- their sequence in the field value is taken into account for higher priority
    select w.myTableid, w.word, (w.Priority + w.comparativeWordIndex + coalesce(sequncedPriority ,0)) as Priority
    from wordIndexRatings w left join
    (
     select w1.myTableid, w1.priority, w1.word, w1.comparativeWordIndex, count(w1.myTableid) as sequncedPriority
     from wordIndexRatings w1 join wordIndexRatings w2 on w1.myTableId = w2.myTableId and w1.Priority > w2.Priority and w1.comparativeWordIndex>w2.comparativeWordIndex
     group by w1.myTableid, w1.priority,w1.word, w1.comparativeWordIndex
    )
    sequencedPriority on w.myTableId = sequencedPriority.myTableId and w.Priority = sequencedPriority.Priority
),
prioritizedSplitWordMatches as ( -- this calculates the cumulative priority for a field value
select  w1.myTableId, sum(w1.Priority) as OverallPriority from wordIndexSequenceRatings w1 join wordIndexSequenceRatings w2 on w1.myTableId =  w2.myTableId
where w1.word <> w2.word group by w1.myTableid
),
completeSet as (
select myTableid, priority from plainMatches -- get plain matches which should be highest ranked
union
select myTableid, OverallPriority as priority from prioritizedSplitWordMatches -- get ranked split word matches (which are ordered based on word rank in search string and sequence)
),
maximizedCompleteSet as( -- set the priority of a field value = maximum priority for that field value
select myTableid, max(priority) as Priority  from completeSet group by myTableId
)
select priority, myTable.myTableid , code, Description from maximizedCompleteSet m join myTable  on m.myTableId = myTable.myTableId
order by Priority desc, Description -- order by priority desc to get highest rated items on top
--offset 0 rows fetch next 50 rows only -- optional paging

Sooner or later, you want to know when a column contains in SQL another value.  In this article we’ll go over several ways you can test to see whether one value is contained within an another.

What makes this problem different, is that we aren’t looking for an exact match, rather, we’re looking for a value within another. Consider the production description:

 “Mountain Bike with lightweight frame yet rugged enough to tackle the toughest trails.”

What can we use in SQL to test whether the description contains the word “frame?”

Table of contents

  • Contains in SQL Video Lesson
  • Use LIKE and Pattern Matching
  • CHARINDEX() to Find Position of Word
  • PATINDEX() To Find Position of Pattern
  • Interesting Use of IN with STRING_SPLIT()
  • Full Text Search with CONTAINS() in SQL
  • Conclusion

Contains in SQL Video Lesson

Before we dig into the details, don’t forget to check out the accompanying video.  I go over all the example there to help you understand what is going on with the queries.

Today we’ll go over five ways you can do this test.  At the end, I’ll let you know which one I like best, but be sure to check out every approach, as you get to learn more SQL in the process.

Use LIKE and Pattern Matching

You can use the  LIKE operator to match against a pattern.  You can use wildcard characters to match for word and characters within a column.

To find all Product Names containing ‘Frame’ we could use:

select ProductID, ProductNumber, Name ProductName
from Production.Product
where Name like '%frame%'

Check out the pattern.  The percent signs (%) instruct the pattern to match zero or more characters.   In English, this pattern says, “look for ‘frame’  don’t worry what comes before or after it.”

Want to dig deeper?  Read SQL WHERE LIKE

CHARINDEX() to Find Position of Word

CHARINDEX() is a TSLQ function that returns the starting position of a character expression in another character expression.  We can use this to help us figure out whether our SQL columns contain the searched text.

Continuing with our example if we’re looking for “Frame” we can use this query:

select ProductID, ProductNumber, Name ProductName
from Production.Product
where CHARINDEX('frame', name) > 0

This works as it CHARINDEX() returns a values greater than 0 if “frame” is found within the column name.  If the value isn’t found, zero is returned.

PATINDEX() To Find Position of Pattern

The PATINDEX() TSQL function is very similar to CHARINDEX(); however, it can take a pattern for matching.  This is  how LIKE does its matching.  They use the same pattern matching ideas.

To find “frame” within Product.Name use this query along with PATINDEX():

select ProductID, ProductNumber, Name ProductName
from Production.Product
where PATINDEX('%frame%', name) > 0

To learn more about PATINDEX check out my article SQL PATINDEX() Function.

Interesting Use of IN with STRING_SPLIT()

You may think you can use the IN clause to find Frame, and in a way you’re partially correct.  But one thing is for sure, this query will not work!

select ProductID, ProductNumber, Name ProductName
from Production.Product
where name in ('frame')

The problem is the filter does first break name into word before comparing to “frame.”  Luckily there is a SQL function that addresses this issue.

To get around this dilemma we’ll use STRING_SPLIT().  Unlike other functions, which output a single value, STRING_SPLIT() returns a table.

Is this example you can see where we split the string “Hail to the Victors” into four separate words.

contains in sql STRING_SPLIT example

Knowing we get a table returned, let’s write a subquery and use the IN operator.  We’ll test whether STRING_SPLIT() includes “frame”  within its results:

select ProductID, ProductNumber, Name ProductName
from Production.Product
where 'frame' in (select value from STRING_SPLIT(name, ' ') )

There are couple of steps taking place:

  1. STRING_SPLIT() returns a table of words within the Product.Name
  2. The subquery test to see whether “frame” is within this table, if so the IN comparison returns TRUE.

If this one seems confusing, then be sure to watch my YouTube Video, as I go this this example in detail for you to understand.

Full Text Search with CONTAINS() in SQL

The final solution we’ll talk about is CONTAINS(). It return true if one word is contained within another phrase, such as a our column Product.Name.

It seems like the most obvious choice to use, until you realize it requires some upfront work.

Since CONTAINS relys on SQL’s full text capabilities, you’ll need to ensure those features are installed in the DBMS.  Also, inorder to use on a column, the column requires a FULLTEXT index.

Here is how you can set up the index:

create fulltext catalog ft as default
create fulltext index on Production.Product(name) key index ak_product_name with stoplist=system

Once that is complete, the query is straight forward:

select ProductID, ProductNumber, Name ProductName
from Production.Product
where CONTAINS(name, 'frame')

This technique has potential, especially if you need to search massive full text such as level depositions.  If this is something you’re looking to do, then check out Microsoft’s Full Text Search article.

Conclusion

Out of all the techniques listed the one I would most likely use is is LIKE. When it comes to queries it is easy to setup and its pattern matching capabilities are handy.

With that said, don’t discount the other techniques. Thought I would use them in a query all the time, the functions are handy and I do use them from time to time.

The COUNTIF function counts cells that meet supplied criteria, and returns a count of occurrences found. If no cells meet criteria, COUNTIF returns zero.

The asterisk (*) is a wildcard for one or more characters. By concatenating asterisks before and after the value in D5, the formula will count the value as a substring. In other words, it will count the value if it appears anywhere inside any cell in the range.

Any positive result means the value was found. By comparing the result with the greater than operator (>) and zero, we force a final result of TRUE or FALSE.

With IF

You can nest this formula inside the IF function as the logical test. For example, to return a final result of «Yes» or «No», you can use IF like this:

=IF(COUNTIF(range,"*"&value&"*"),"Yes","No")
Skip to content

Excel Logo

Excel If Cell Contains Text

Excel If Cell Contains Text Then

Excel If Cell Contains Text Then Formula helps you to return the output when a cell have any text or a specific text. You can check if a cell contains a some string or text  and produce something in other cell. For Example you can check if a cell A1 contains text ‘example text’  and print Yes or No in Cell B1. Following are the example Formulas to check if Cell contains text then return some thing in a Cell.

If Cell Contains Text

Here are the Excel formulas to check if Cell contains specific text then return something. This will return if there is any string or any text in given Cell. We can use this simple approach to check if a cell contains text, specific text, string,  any text using Excel If formula. We can use equals to  operator(=) to compare the strings .

If Cell Contains Text Then TRUE

Following is the Excel formula to return True if a Cell contains Specif Text. You can check a cell if there is given string in the Cell and return True or False.

=IF(ISNUMBER(FIND(“How”,A1,1)),TRUE,FALSE)

The formula will return true if it found the match, returns False of no match found.

If Cell Contains Text Then TRUE

If Cell Contains Partial Text

We can return Text If Cell Contains Partial Text. We use formula or VBA to Check Partial Text in a Cell.

Find for Case Sensitive Match:

We can check if a Cell Contains Partial Text then return something using Excel Formula. Following is a simple example to find the partial text in a given Cell. We can use if your want to make the criteria case sensitive.

=IF(ISERROR(FIND($E$1,A2,1)),”Not Found”,”Found”)

If Cell Contains Partial Text

  • Here, Find Function returns the finding position of the given string
  • Use Find function is Case Sensitive
  • IsError Function check if Find Function returns Error, that means, string not found

Search for Not Case Sensitive Match:

We can use Search function to check if Cell Contains Partial Text. Search function useful if you want to make the checking criteria Not Case Sensitive.

=IF(ISERROR(SEARCH($F$1,A2,1)),”Not Found”,”Found”)

If Cell Contains Partial Text Not Case Sensitive

If Range of Cells Contains Text

We can check for the strings in a range of cells. Here is the formula to find If Range of Cells Contains Text. We can use Count If Formula to check the excel if range of cells contains specific text and return Text.

=IF(COUNTIF(A2:A21, “*Region 1d*”)>0,”Range Contais Text”,”Text Not Found in the Given Range”)
  • CountIf function counts the number of cells with given criteria
  • We can use If function to return the required Text
  • Formula displays the Text ‘Range Contains Text” if match found
  • Returns “Text Not Found in the Given Range” if match not found in the specified range

If Cells Contains Text From List

Below formulas returns text If Cells Contains Text from given List. You can use based on your requirement.

VlookUp to Check If Cell Contains Text from a List:
We can use VlookUp function to match the text in the Given list of Cells. And return the corresponding values.

  • Check if a List Contains Text:
    =IF(ISERR(VLOOKUP(F1,A1:B21,2,FALSE)),”False:Not Contains”,”True: Text Found”)
  • Check if a List Contains Text and Return Corresponding Value:
    =VLOOKUP(F1,A1:B21,2,FALSE)
  • Check if a List Contains Partial Text and Return its Value:
    =VLOOKUP(“*”&F1&”*”,A1:B21,2,FALSE)

If Cell Contains Text Then Return a Value

We can return some value if cell contains some string. Here is the the the Excel formula to return a value if a Cell contains Text. You can check a cell if there is given string in the Cell and return some string or value in another column.

If Cell Contains Text Then Return a Value

=IF(ISNUMBER(SEARCH(“How”,A1,1)),”Found”,”Not Found”)

The formula will return true if it found the match, returns False of no match found. can

Excel if cell contains word then assign value

You can replace any word in the following formula to check if cell contains word then assign value.

=IFERROR(IF(SEARCH(“Word”,A2,1)>0,1,0),””)

Excel if cell contains word then assign value
Search function will check for a given word in the required cell and return it’s position. We can use If function to check if the value is greater than 0 and assign a given value (example: 1) in the cell. search function returns #Value if there is no match found in the cell, we can handle this using IFERROR function.

Count If Cell Contains Text

We can check If Cell Contains Text Then COUNT. Here is the Excel formula to Count if a Cell contains Text. You can count the number of cells containing specific text.

=COUNTIF($A$2:$A$7,”*”&D2&”*”)

The formula will Sum the values in Column B if the cells of Column A contains the given text.

If Cell Contains Text Then COUNT

Count If Cell Contains Partial Text

We can count the cells based on partial match criteria. The following Excel formula Counts if a Cell contains Partial Text.

=COUNTIF(A2:A21, “*Region 1*”)
  • We can use the CountIf Function to Count the Cells if they contains given String
  • Wild-card operators helps to make the CountIf to check for the Partial String
  • Put Your Text between two asterisk symbols (*YourText*) to make the criteria to find any where in the given Cell
  • Add Asterisk symbol at end of your text (YourText*) to make the criteria to find your text beginning of given Cell
  • Place Asterisk symbol at beginning of your text (*YourText) to make the criteria to find your text end of given Cell

If Cell contains text from list then return value

Here is the Excel Formula to check if cell contains text from list then return value. We can use COUNTIF and OR function to check the array of values in a Cell and return the given Value. Here is the formula to check the list in range D2:D5 and check in Cell A2 and return value in B2.

=IF(OR(COUNTIF(A2,”*”&$D$2:$D$5&”*”)), “Return Value”, “”)

Excel If cell contains text from list then return value

If Cell Contains Text Then SUM

Following is the Excel formula to Sum if a Cell contains Text. You can total the cell values if there is given string in the Cell. Here is the example to sum the column B values based on the values in another Column.

=SUMIF($A$2:$A$7,”*”&D2&”*”,$B$2:$B$7)

The formula will Sum the values in Column B if the cells of Column A contains the given text.

If Cell Contains Text Then SUM

Sum If Cell Contains Partial Text

Use SumIfs function to Sum the cells based on partial match criteria. The following Excel formula Sums the Values if a Cell contains Partial Text.

=SUMIFS(C2:C21,A2:A21, “*Region 1*”)
  • SUMIFS Function will Sum the Given Sum Range
  • We can specify the Criteria Range, and wild-card expression to check for the Partial text
  • Put Your Text between two asterisk symbols (*YourText*) to Sum the Cells if the criteria to find any where in the given Cell
  • Add Asterisk symbol at end of your text (YourText*) to Sum the Cells if the criteria to find your text beginning of given Cell
  • Place Asterisk symbol at beginning of your text (*YourText) to Sum the Cells if criteria to find your text end of given Cell

VBA to check if Cell Contains Text

Here is the VBA function to find If Cells Contains Text using Excel VBA Macros.

If Cell Contains Partial Text VBA

We can use VBA to check if Cell Contains Text and Return Value. Here is the simple VBA code match the partial text. Excel VBA if Cell contains partial text macros helps you to use in your procedures and functions.

Sub sbCkeckforPartialText()
MsgBox CheckIfCellContainsPartialText(Cells(2, 1), “Region 1”)
End Sub
Function CheckIfCellContainsPartialText(ByVal cell As Range, ByVal strText As String) As Boolean
If InStr(1, cell.Value, strText) > 0 Then CheckIfCellContainsPartialText = True
End Function
  • CheckIfCellContainsPartialText VBA Function returns true if Cell Contains Partial Text
  • inStr Function will return the Match Position in the given string

If Cell Contains Text Then VBA MsgBox

Here is the simple VBA code to display message box if cell contains text. We can use inStr Function to search for the given string. And show the required message to the user.

Sub sbVBAIfCellsContainsText()
If InStr(1, Cells(2, 1), “Region 3”) > 0 Then blnMatch = True
If blnMatch = True Then MsgBox “Cell Contains Text”
End Sub
  • inStr Function will return the Match Position in the given string
  • blnMatch is the Boolean variable becomes True when match string
  • You can display the message to the user if a Range Contains Text

Which function returns true if cell a1 contains text?

You can use the Excel If function and Find function to return TRUE if Cell A1 Contains Text. Here is the formula to return True.

=IF(ISNUMBER(FIND(“YourText”,A1,1)),TRUE,FALSE)

Which function returns true if cell a1 contains text value?

You can use the Excel If function with Find function to return TRUE if a Cell A1 Contains Text Value. Below is the formula to return True based on the text value.

=IF(ISNUMBER(FIND(“YourTextValue”,A1,1)),TRUE,FALSE)

Share This Story, Choose Your Platform!

7 Comments

  1. Meghana
    December 27, 2019 at 1:42 pm — Reply

    Hi Sir,Thank you for the great explanation, covers everything and helps use create formulas if cell contains text values.

    Many thanks! Meghana!!

  2. Max
    December 27, 2019 at 4:44 pm — Reply

    Perfect! Very Simple and Clear explanation. Thanks!!

  3. Mike Song
    August 29, 2022 at 2:45 pm — Reply

    I tried this exact formula and it did not work.

  4. Theresa A Harding
    October 18, 2022 at 9:51 pm — Reply
  5. Marko
    November 3, 2022 at 9:21 pm — Reply

    Hi

    Is possible to sum all WA11?

    (A1) WA11 4

    (A2) AdBlue 1, WA11 223

    (A3) AdBlue 3, WA11 32, shift 4

    … and everything is in one column.

    Thanks you very much for your help.

    Sincerely Marko

  6. Mike
    December 9, 2022 at 9:59 pm — Reply

    Thank you for the help. The formula =OR(COUNTIF(M40,”*”&Vendors&”*”)) will give “TRUE” when some part of M40 contains a vendor from “Vendors” list. But how do I get Excel to tell which vendor it found in the M40 cell?

    • PNRao
      December 18, 2022 at 6:05 am — Reply

      Please describe your question more elaborately.
      Thanks!

© Copyright 2012 – 2020 | Excelx.com | All Rights Reserved

Page load link

Problem Statement

Given a Pandas Dataframe, we need to check if a particular column contains a certain string or not.


Overview

A column is a Pandas Series so we can use amazing Pandas.Series.str from Pandas API which provide tons of useful string utility functions for Series and Indexes.

We will use Pandas.Series.str.contains() for this particular problem.

Series.str.contains()

  • Syntax: Series.str.contains(string), where string is string we want the match for.
  • Parameters: A string or a regular expression.
  • Return Value: It returns a boolean series of size len(dataframe) based on whether the string or regex(parameter) is contained within the string of Series or Index.

Let’s see how we can use the above method using some examples

Implementation using Series.str.contains()
import pandas as pd  # Importing Pandas 

# Make a test dataframe 
df = pd.DataFrame({
        'a' : ['rick','johnatthan','katei','diana','richard'],
        'b' : ['rich','roman','italy','ed','taylor'],
        'c' : ['beyonce','linkinpark','fortminor','mariahcarey','jlo']
    })

# Find string 'diana' in column 'a'

boolean_findings = df['a'].str.contains('diana')

"""
Returns a boolean series of size len(dataframe).
One boolean for each row of column, True if string is contained within, false otherwise
"""
print(boolean_findings)
#Output
0    False
1    False
2    False
3     True
4    False
Name: a, dtype: bool

total_occurence = boolean_findings.sum()  # Returns count of all boolean true.

if(total_occurence > 0):
    print("Yes the string is present in the column")

We can also use Pandas.series.any() too if we are not concerned about the number of occurrences of the string.

any() returns True if any element of the iterable is True(or exists). So, it can only check if the string is present within the strings of the column.

Example using Pandas.series.any()
import pandas as pd  # Importing Pandas 

# Make a test dataframe 
df = pd.DataFrame({
        'a' : ['rick','johnatthan','katei','diana','richard'],
        'b' : ['rich','roman','italy','ed','taylor'],
        'c' : ['beyonce','linkinpark','fortminor','mariahcarey','jlo']
    })

# Find string 'diana' in column 'a'
boolean_finding = df['a'].str.contains('diana').any()

# Returns true if the 
print(boolean_finding)

#Output 
# True

if(boolean_finding):
    print("Yes the string is present in the column")

#Output
# Yes the string is present in the column

Note: Pandas.str.contain() returns True even if the string(parameter) is a part of the string present in the column i.e it is a substring of a string in the Pandas column. For example: If we had searched for ‘dia’ in place of ‘diana’ in the column 'a', then str.contains() still would have returned True.

If we want to find only the exact matches, then we can do something like this:

import pandas as pd  # Importing Pandas 

# Make a test dataframe 
df = pd.DataFrame({
        'a' : ['rick','johnatthan','katei','diana','richard'],
        'b' : ['rich','roman','italy','ed','taylor'],
        'c' : ['beyonce','linkinpark','fortminor','mariahcarey','jlo']
    })

total_presence = len(df[df['a'] == 'diana'])

print(total_presence)
#Output
# 1

if(total_presence):
    print("Yes the string is present with the exact match")

#Output 
# Yes the string is present with the exact match

Suggested Reading

  • Remove duplicate rows from a Pandas Dataframe (Difficulty – Easy)
  • Merge two text columns into a single column in a Pandas Dataframe (Difficulty – Medium)
  • Delete the entire row if any column has NaN in a Pandas Dataframe (Difficulty – Easy)
  • Difference between map(), apply() and applymap() in Pandas (Difficulty – Medium)

That’s all, folks !!!

Понравилась статья? Поделить с друзьями:
  • If a word or phrase in bold is correct put a tick on monday
  • If color function in excel
  • If a word ends in vowel consonant
  • If clause for excel
  • If a and b are true excel