web-dev-qa-db-fra.com

Comment convertir des données en cas approprié dans SQL Server

SQL Server contient des fonctions système pour afficher/mettre à jour les données de chaîne en majuscules et en minuscules, mais pas en casse appropriée. Il existe plusieurs raisons pour lesquelles cette opération a lieu dans SQL Server plutôt qu'au niveau de la couche application. Dans mon cas, nous effectuions un nettoyage des données lors d'une consolidation de nos données RH mondiales à partir de plusieurs sources.

Si vous recherchez sur Internet, vous trouverez plusieurs solutions à cette tâche, mais beaucoup semblent avoir des mises en garde restrictives ou ne permettent pas de définir des exceptions dans la fonction.

NOTE : Comme mentionné dans les commentaires ci-dessous, SQL Server n'est PAS l'endroit idéal pour effectuer cette conversion. D'autres méthodes ont également été suggérées - comme le CLR, par exemple. À mon avis, ce message a atteint son objectif - c'est génial d'avoir toutes ces pensées en un seul endroit, par opposition aux friandises aléatoires disponibles ici et là. Merci à tous.

5
SQL_Underworld

La meilleure solution que j'ai trouvée peut être trouvée ici .

J'ai modifié un peu le script: j'ai ajouté LTRIM et RTRIM à la valeur renvoyée car, dans certains cas, le script ajoutait des espaces après la valeur.

Exemple d'utilisation pour prévisualiser la conversion des données UPPERCASE en cas approprié, avec des exceptions:

SELECT <column>,[dbo].[fProperCase](<column>,'|APT|HWY|BOX|',NULL)
FROM <table> WHERE <column>=UPPER(<column>)

L'aspect vraiment simple mais puissant de ce script est la possibilité de définir des exceptions dans l'appel de fonction lui-même.

Une mise en garde cependant:
Tel qu'il est actuellement écrit, le script ne gère pas correctement les noms de famille Mc [A-Z]%, Mac [A-Z]%, etc. Je travaille actuellement sur des modifications pour gérer ce scénario.

En guise de solution, j'ai changé le paramètre renvoyé par la fonction: REPLACE (REPLACE (LTRIM (RTRIM ((@ ProperCaseText)))), 'Mcd', 'McD'), 'Mci', 'McI'), etc. ...

Cette méthode nécessitait évidemment une connaissance préalable des données et n'est pas idéale. Je suis sûr qu'il existe un moyen de résoudre ce problème, mais je suis au milieu d'une conversion et je n'ai actuellement pas le temps de consacrer à ce problème embêtant.

Voici le code:

CREATE FUNCTION [dbo].[fProperCase](@Value varchar(8000), @Exceptions varchar(8000),@UCASEWordLength tinyint)
returns varchar(8000)
as
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Function Purpose: To convert text to Proper Case.
Created By:             David Wiseman
Website:                http://www.wisesoft.co.uk
Created:                2005-10-03
Updated:                2006-06-22
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
INPUTS:

@Value :                This is the text to be converted to Proper Case
@Exceptions:            A list of exceptions to the default Proper Case rules. e.g. |RAM|CPU|HDD|TFT|
                              Without exception list they would display as Ram, Cpu, Hdd and Tft
                              Note the use of the Pipe "|" symbol to separate exceptions.
                              (You can change the @sep variable to something else if you prefer)
@UCASEWordLength: You can specify that words less than a certain length are automatically displayed in UPPERCASE

USAGE1:

Convert text to ProperCase, without any exceptions

select dbo.fProperCase('THIS FUNCTION WAS CREATED BY DAVID WISEMAN',null,null)
>> This Function Was Created By David Wiseman

USAGE2:

Convert text to Proper Case, with exception for WiseSoft

select dbo.fProperCase('THIS FUNCTION WAS CREATED BY DAVID WISEMAN @ WISESOFT','|WiseSoft|',null)
>> This Function Was Created By David Wiseman @ WiseSoft

USAGE3:

Convert text to Proper Case and default words less than 3 chars to UPPERCASE

select dbo.fProperCase('SIMPSON, HJ',null,3)
>> Simpson, HJ

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
begin
      declare @sep char(1) -- Seperator character for exceptions
      declare @i int -- counter
      declare @ProperCaseText varchar(5000) -- Used to build our Proper Case string for Function return
      declare @Word varchar(1000) -- Temporary storage for each Word
      declare @IsWhiteSpace as bit -- Used to indicate whitespace character/start of new Word
      declare @c char(1) -- Temp storage location for each character

      set @Word = ''
      set @i = 1
      set @IsWhiteSpace = 1
      set @ProperCaseText = ''
      set @sep = '|'

      -- Set default UPPERCASEWord Length
      if @UCASEWordLength is null set @UCASEWordLength = 1
      -- Convert user input to lower case (This function will UPPERCASE words as required)
      set @Value = LOWER(@Value)

      -- Loop while counter is less than text lenth (for each character in...)
      while (@i <= len(@Value)+1)
      begin

            -- Get the current character
            set @c = SUBSTRING(@Value,@i,1)

            -- If start of new Word, UPPERCASE character
            if @IsWhiteSpace = 1 set @c = UPPER(@c)

            -- Check if character is white space/symbol (using ascii values)
            set @IsWhiteSpace = case when (ASCII(@c) between 48 and 58) then 0
                                          when (ASCII(@c) between 64 and 90) then 0
                                          when (ASCII(@c) between 96 and 123) then 0
                                          else 1 end

            if @IsWhiteSpace = 0
            begin
                  -- Append character to temp @Word variable if not whitespace
                  set @Word = @Word + @c
            end
            else
            begin
                  -- Character is white space/punctuation/symbol which marks the end of our current Word.
                  -- If Word length is less than or equal to the UPPERCASE Word length, convert to upper case.
                  -- e.g. you can specify a @UCASEWordLength of 3 to automatically UPPERCASE all 3 letter words.
                  set @Word = case when len(@Word) <= @UCASEWordLength then UPPER(@Word) else @Word end

                  -- Check Word against user exceptions list. If exception is found, use the case specified in the exception.
                  -- e.g. WiseSoft, RAM, CPU.
                  -- If Word isn't in user exceptions list, check for "known" exceptions.
                  set @Word = case when charindex(@sep + @Word + @sep,@exceptions collate Latin1_General_CI_AS) > 0
                                    then substring(@exceptions,charindex(@sep + @Word + @sep,@exceptions collate Latin1_General_CI_AS)+1,len(@Word))
                                    when @Word = 's' and substring(@Value,@i-2,1) = '''' then 's' -- e.g. Who's
                                    when @Word = 't' and substring(@Value,@i-2,1) = '''' then 't' -- e.g. Don't
                                    when @Word = 'm' and substring(@Value,@i-2,1) = '''' then 'm' -- e.g. I'm
                                    when @Word = 'll' and substring(@Value,@i-3,1) = '''' then 'll' -- e.g. He'll
                                    when @Word = 've' and substring(@Value,@i-3,1) = '''' then 've' -- e.g. Could've
                                    else @Word end

                  -- Append the Word to the @ProperCaseText along with the whitespace character
                  set @ProperCaseText = @ProperCaseText + @Word + @c
                  -- Reset the Temp @Word variable, ready for a new Word
                  set @Word = ''
            end
            -- Increment the counter
            set @i = @i + 1
      end
      return @ProperCaseText
end
2
SQL_Underworld

Le défi que vous rencontrerez avec ces approches est que vous avez perdu des informations. Expliquez aux utilisateurs professionnels qu'ils ont pris une image floue, floue et malgré ce qu'ils voient sur t.v. il n'y a aucun moyen de le rendre net et net. Il y aura toujours des situations où ces règles ne fonctionneront pas et tant que tout le monde sait que c'est le cas, alors allez-y.

Il s'agit de données RH, je vais donc supposer que nous parlons d'obtenir des noms dans un format de casse de titre cohérent, car l'ordinateur central les stocke sous la forme AARON BERTRAND et nous voulons que le nouveau système ne leur crie pas dessus. Aaron est facile (mais pas bon marché). Vous et Max avez déjà identifié le problème avec le Mc/Mac, donc il capitalise correctement Mc/Mac mais il y a des cas où il est trop agressif avec Mackey/ Maclin /Mackenzie. Mackenzie est un cas intéressant - regardez comment sa popularité a explosé en tant que nom du bébé

Mackenzie

À un moment donné, il y aura un pauvre enfant nommé Mackenzie MacKenzie parce que les gens sont des êtres horribles.

Vous allez également rencontrer de belles choses comme D'Antoni où nous devrions coiffer les deux lettres autour de la coche. Sauf pour d'Autremont où vous ne mettez la lettre en majuscule qu'après l'apostrophe. Le ciel vous aide cependant si vous envoyez un courrier à d'Illoni car leur nom de famille est D'illoni.

Afin de contribuer au code réel, voici une méthode CLR que nous avons utilisée dans une instance de 2005 pour nos besoins. Il a généralement utilisé ToTitleCase, à l'exception de la liste des exceptions que nous avons élaborées, c'est-à-dire lorsque nous avons essentiellement renoncé à codifier les exceptions susmentionnées.

namespace Common.Util
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;

    /// <summary>
    /// A class that attempts to proper case a Word, taking into
    /// consideration some outliers.
    /// </summary>
    public class ProperCase
    {
        /// <summary>
        /// Convert a string into its propercased equivalent.  General case
        /// it will capitalize the first letter of each Word.  Handled special 
        /// cases include names with apostrophes (O'Shea), and Scottish/Irish
        /// surnames MacInnes, McDonalds.  Will fail for Macbeth, Macaroni, etc
        /// </summary>
        /// <param name="inputText">The data to be recased into initial caps</param>
        /// <returns>The input text resampled as proper cased</returns>
        public static string Case(string inputText)
        {
            CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture;
            TextInfo textInfo = cultureInfo.TextInfo;
            string output = null;
            int staticHack = 0;

            Regex expression = null;
            string matchPattern = string.Empty;

            // Should think about maybe matching the first non blank character
            matchPattern = @"
                (?<Apostrophe>'.\B)| # Match things like O'Shea so apostrophe plus one.  Think about white space between ' and next letter.  TODO:  Correct it's from becoming It'S, can't -> CaN'T
                \bMac(?<Mac>.) | # MacInnes, MacGyver, etc.  Will fail for Macbeth
                \bMc(?<Mc>.) # McDonalds
                ";
            expression = new Regex(matchPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);

            // Handle our funky rules            
            // Using named matches is probably overkill as the
            // same rule applies to all but for future growth, I'm
            // defining it as such.
            // Quirky behaviour---for 2005, the compiler will 
            // make this into a static method which is verboten for 
            // safe assemblies.  
            MatchEvaluator upperCase = delegate(Match match)
            {
                // Based on advice from Chris Hedgate's blog
                // I need to reference a local variable to prevent
                // this from being turned into static
                staticHack = matchPattern.Length;

                if (!string.IsNullOrEmpty(match.Groups["Apostrophe"].Value))
                {
                    return match.Groups["Apostrophe"].Value.ToUpper();
                }

                if (!string.IsNullOrEmpty(match.Groups["Mac"].Value))
                {
                    return string.Format("Mac{0}", match.Groups["Mac"].Value.ToUpper());
                }

                if (!string.IsNullOrEmpty(match.Groups["Mc"].Value))
                {
                    return string.Format("Mc{0}", match.Groups["Mc"].Value.ToUpper());
                }

                return match.Value;
            };

            MatchEvaluator evaluator = new MatchEvaluator(upperCase);

            if (inputText != null)
            {
                // Generally, title casing converts the first character 
                // of a Word to uppercase and the rest of the characters 
                // to lowercase. However, a Word that is entirely uppercase, 
                // such as an acronym, is not converted.
                // http://msdn.Microsoft.com/en-us/library/system.globalization.textinfo.totitlecase(VS.80).aspx
                string temporary = string.Empty;
                temporary = textInfo.ToTitleCase(inputText.ToString().ToLower());
                output = expression.Replace(temporary, evaluator);
            }
            else
            {
                output = string.Empty;
            }

            return output;
        }
    }
}

Maintenant que tout cela est clair, je vais finir ce joli livre de poésie par e e cummings

11
billinkc

Je me rends compte que vous avez déjà une bonne solution, mais je pensais que j'ajouterais une solution plus simple en utilisant une fonction de valeur de table en ligne, bien que celle qui repose sur l'utilisation de la prochaine version "vNext" de SQL Server, qui inclut le STRING_AGG() et STRING_SPLIT() fonctions:

IF OBJECT_ID('dbo.fn_TitleCase') IS NOT NULL
DROP FUNCTION dbo.fn_TitleCase;
GO
CREATE FUNCTION dbo.fn_TitleCase
(
    @Input nvarchar(1000)
)
RETURNS TABLE
AS
RETURN
SELECT Item = STRING_AGG(splits.Word, ' ')
FROM (
    SELECT Word = UPPER(LEFT(value, 1)) + LOWER(RIGHT(value, LEN(value) - 1))
    FROM STRING_SPLIT(@Input, ' ')
    ) splits(Word);
GO

Test de la fonction:

SELECT *
FROM dbo.fn_TitleCase('this is a test');

C'est un test

SELECT *
FROM dbo.fn_TitleCase('THIS IS A TEST');

C'est un test

Voir MSDN pour la documentation sur STRING_AGG () et STRING_SPLIT ()

Gardez à l'esprit que la fonction STRING_SPLIT() ne garantit pas le retour des articles dans un ordre particulier. Cela peut être très ennuyeux. Il existe un élément Microsoft Feedback demandant qu'une colonne soit ajoutée à la sortie de STRING_SPLIT pour indiquer l'ordre de la sortie. Pensez à voter pour ici

Si vous souhaitez vivre sur Edge et que vous souhaitez utiliser cette méthodologie, elle peut être développée pour inclure des exceptions. J'ai construit une fonction de valeur de table en ligne qui fait exactement cela:

CREATE FUNCTION dbo.fn_TitleCase
(
    @Input nvarchar(1000)
    , @SepList nvarchar(1)
)
RETURNS TABLE
AS
RETURN
WITH Exceptions AS (
    SELECT v.ItemToFind
        , v.Replacement
    FROM (VALUES /* add further exceptions to the list below */
          ('mca', 'McA')
        , ('maca','MacA')
        ) v(ItemToFind, Replacement)
)
, Source AS (
    SELECT Word = UPPER(LEFT(value, 1 )) + LOWER(RIGHT(value, LEN(value) - 1))
        , Num = ROW_NUMBER() OVER (ORDER BY GETDATE())
    FROM STRING_SPLIT(@Input, @SepList) 
)
SELECT Item = STRING_AGG(splits.Word, @SepList)
FROM (
    SELECT TOP 214748367 Word
    FROM (
        SELECT Word = REPLACE(Source.Word, Exceptions.ItemToFind, Exceptions.Replacement)
            , Source.Num
        FROM Source
        CROSS APPLY Exceptions
        WHERE Source.Word LIKE Exceptions.ItemToFind + '%'
        UNION ALL
        SELECT Word = Source.Word
            , Source.Num
        FROM Source
        WHERE NOT EXISTS (
            SELECT 1
            FROM Exceptions
            WHERE Source.Word LIKE Exceptions.ItemToFind + '%'
            )
        ) w
    ORDER BY Num
    ) splits;
GO

Le test montre comment cela fonctionne:

SELECT *
FROM dbo.fn_TitleCase('THIS IS A TEST MCADAMS MACKENZIE MACADAMS', ' ');

Ceci est un test McAdams Mackenzie MacAdams

6
Max Vernon