74

I need to replace Microsoft Word's version of single and double quotations marks (“ ” ‘ ’) with regular quotes (' and ") due to an encoding issue in my application. I do not need them to be HTML entities and I cannot change my database schema.

I have two options: to use either a regular expression or an associated array.

Is there a better way to do this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Misha M
  • 10,979
  • 17
  • 53
  • 65
  • IMO, best answer specifically for smart quote handling is here: https://stackoverflow.com/a/21491305/597034 – John Rix Sep 18 '19 at 16:12

6 Answers6

120

I have found an answer to this question. You need just one line of code using iconv() function in php:

// replace Microsoft Word version of single  and double quotations marks (“ ” ‘ ’) with  regular quotes (' and ")
$output = iconv('UTF-8', 'ASCII//TRANSLIT', $input);     
Fosco
  • 38,138
  • 7
  • 87
  • 101
Justin Dominic
  • 1,227
  • 1
  • 10
  • 22
  • if my answer helped u can u upvote my original question http://stackoverflow.com/questions/6597268/i-want-to-convert-the-posted-value-hello-to-hello-in-zend-framework – Justin Dominic May 14 '12 at 13:47
  • 10
    Thanks however in my case I needed to pick the right character encoding (which was CP1252 and not UTF-8): `$output = iconv('CP1252', 'ASCII//TRANSLIT', $input);` – Eric Kigathi Jul 17 '12 at 22:02
  • 1
    @eric good to know you used your mind on it for others. thanks for sharing :) – Justin Dominic Aug 08 '12 at 11:41
  • 3
    Yep this worked for me. I'd recommend this over the accepted answer :) – Ben Sinclair Nov 24 '13 at 10:38
  • This works for me while the accepted answer doesn't. I would like to change this one to accepted answer. – Ngoc Pham Feb 01 '14 at 01:48
  • But are the Microsoft quotes Unicode code points or CP1252 code points? If the latter, this solution will not work. Actually, it will throw a notice: `PHP Notice: iconv(): Detected an illegal character in input string in php shell code on line 1`. – NobleUplift Apr 23 '14 at 18:23
  • Worked flawlessly, thank you ! im on UTF-8 charset, files encoded and also utf8-bin in database... thanks! – eljamz Apr 20 '17 at 23:09
  • This seems a good (or, lazy) solution to my problem in Zen Cart, customers entering curly quotes in their names when signing up, and ZC stores first and last name in the PHP session, which then fails to decode with "PHP Warning: session_start(): Failed to decode session object. Session has been destroyed" message. I'm going to work around by stripping the strings with `iconv` before saving them to the database during account creation. – Neek Aug 21 '18 at 09:28
  • WARNING: iconv is a PHP extension and may not be installed on your production environment! "`Fatal error: Call to undefined function iconv()`" Be sure to test your code on every platform it needs to run. – Neek Aug 22 '18 at 13:28
  • 1
    This is a good trick. but I notice this solution removes special characters like é, è, à, â and others. Any solution to shirk the issue? – Cutis Oct 14 '19 at 10:05
  • January 2022 this worked for me in PHP 5.6 – Professor Zoom Feb 01 '22 at 22:50
91

Considering you only want to replace a few specific and well identified characters, I would go for str_replace with an array: you obviously don't need the heavy artillery regex will bring you ;-)

And if you encounter some other special characters (damn copy-paste from Microsoft Word...), you can just add them to that array whenever is necessary / whenever they are identified.


The best answer I can give to your comment is probably this link: Convert Smart Quotes with PHP

And the associated code (quoting that page):

function convert_smart_quotes($string) 
{ 
    $search = array(chr(145), 
                    chr(146), 
                    chr(147), 
                    chr(148), 
                    chr(151)); 

    $replace = array("'", 
                     "'", 
                     '"', 
                     '"', 
                     '-'); 

    return str_replace($search, $replace, $string); 
} 

(I don't have Microsoft Word on this computer, so I can't test by myself)

I don't remember exactly what we used at work (I was not the one having to deal with that kind of input), but it was the same kind of stuff...

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Pascal MARTIN
  • 395,085
  • 80
  • 655
  • 663
  • 1
    How would you specify the MS characters? – Misha M Aug 11 '09 at 18:17
  • This is what I was looking for. Thanks. The search array did not work as is, I ended up using the Hex version that was provided in the comments from the link you gave above. – Misha M Aug 11 '09 at 18:43
  • The '&' sign copied from MS word doesn't encode properly, is there anyway we can use this snippet to encode that to '&'. (aswell as bullets and other chars) – dotty Oct 06 '09 at 10:31
  • For other users: You might look for `chr(149)` (bullet) and replace it with an asterisk as well. [This page](http://www.java2s.com/Code/VBA-Excel-Access-Word/Data-Type-Functions/UsingtheChrFunctionandConstantstoEnterSpecialCharactersinaString.htm) has a list of several `chr()` characters you might want to convert. – Blazemonger Jun 22 '12 at 14:32
  • You don't check the encoding of the string first, so this function will mangle certain Unicode passed into it. – NobleUplift Apr 23 '14 at 18:47
  • after tearing my hair out trying to figure my encoding issues, this eventually was the ticket for me. i used this (http://php.net/manual/en/function.chr.php) to extend your function for my own purposes - scroll halfway down to the example posted by Josh B. – gorillagoat May 17 '18 at 16:27
44

Your Microsoft-encoded quotes are the probably the typographic quotation marks. You can simply replace them with str_replace if you know the encoding of the string in that you want to replace them.

Here’s an example for UTF-8 but using a single mapping array with strtr:

$quotes = array(
    "\xC2\xAB"     => '"', // « (U+00AB) in UTF-8
    "\xC2\xBB"     => '"', // » (U+00BB) in UTF-8
    "\xE2\x80\x98" => "'", // ‘ (U+2018) in UTF-8
    "\xE2\x80\x99" => "'", // ’ (U+2019) in UTF-8
    "\xE2\x80\x9A" => "'", // ‚ (U+201A) in UTF-8
    "\xE2\x80\x9B" => "'", // ‛ (U+201B) in UTF-8
    "\xE2\x80\x9C" => '"', // “ (U+201C) in UTF-8
    "\xE2\x80\x9D" => '"', // ” (U+201D) in UTF-8
    "\xE2\x80\x9E" => '"', // „ (U+201E) in UTF-8
    "\xE2\x80\x9F" => '"', // ‟ (U+201F) in UTF-8
    "\xE2\x80\xB9" => "'", // ‹ (U+2039) in UTF-8
    "\xE2\x80\xBA" => "'", // › (U+203A) in UTF-8
);
$str = strtr($str, $quotes);

If you’re need another encoding, you can use mb_convert_encoding to convert the keys.

Gumbo
  • 643,351
  • 109
  • 780
  • 844
13

If like me you arrive here with an enormous range of broken ASCII / Microsoft Word characters that are doing weird things to your CMS or RTE and iconv isn't working, then this mad function might just be for you.

Make sure your encoding is UTF-8 when you save this function to a file.

<?php
    /**
     * fixMSWord
     *
     * Replace ASCII chars with UTF-8. Note there are ASCII characters that don't
     * correctly map and will be replaced by spaces.
     *
     * @author      Robin Cafolla
     * @date        2013-03-22
     */
    function fixMSWord($string) {
        $map = Array(
            '33' => '!', '34' => '"', '35' => '#', '36' => '$', '37' => '%', '38' => '&', '39' => "'", '40' => '(', '41' => ')', '42' => '*',
            '43' => '+', '44' => ',', '45' => '-', '46' => '.', '47' => '/', '48' => '0', '49' => '1', '50' => '2', '51' => '3', '52' => '4',
            '53' => '5', '54' => '6', '55' => '7', '56' => '8', '57' => '9', '58' => ':', '59' => ';', '60' => '<', '61' => '=', '62' => '>',
            '63' => '?', '64' => '@', '65' => 'A', '66' => 'B', '67' => 'C', '68' => 'D', '69' => 'E', '70' => 'F', '71' => 'G', '72' => 'H',
            '73' => 'I', '74' => 'J', '75' => 'K', '76' => 'L', '77' => 'M', '78' => 'N', '79' => 'O', '80' => 'P', '81' => 'Q', '82' => 'R',
            '83' => 'S', '84' => 'T', '85' => 'U', '86' => 'V', '87' => 'W', '88' => 'X', '89' => 'Y', '90' => 'Z', '91' => '[', '92' => '\\',
            '93' => ']', '94' => '^', '95' => '_', '96' => '`', '97' => 'a', '98' => 'b', '99' => 'c', '100'=> 'd', '101'=> 'e', '102'=> 'f',
            '103'=> 'g', '104'=> 'h', '105'=> 'i', '106'=> 'j', '107'=> 'k', '108'=> 'l', '109'=> 'm', '110'=> 'n', '111'=> 'o', '112'=> 'p',
            '113'=> 'q', '114'=> 'r', '115'=> 's', '116'=> 't', '117'=> 'u', '118'=> 'v', '119'=> 'w', '120'=> 'x', '121'=> 'y', '122'=> 'z',
            '123'=> '{', '124'=> '|', '125'=> '}', '126'=> '~', '127'=> ' ', '128'=> '&#8364;', '129'=> ' ', '130'=> ',', '131'=> ' ', '132'=> '"',
            '133'=> '.', '134'=> ' ', '135'=> ' ', '136'=> '^', '137'=> ' ', '138'=> ' ', '139'=> '<', '140'=> ' ', '141'=> ' ', '142'=> ' ',
            '143'=> ' ', '144'=> ' ', '145'=> "'", '146'=> "'", '147'=> '"', '148'=> '"', '149'=> '.', '150'=> '-', '151'=> '-', '152'=> '~',
            '153'=> ' ', '154'=> ' ', '155'=> '>', '156'=> ' ', '157'=> ' ', '158'=> ' ', '159'=> ' ', '160'=> ' ', '161'=> '¡', '162'=> '¢',
            '163'=> '£', '164'=> '¤', '165'=> '¥', '166'=> '¦', '167'=> '§', '168'=> '¨', '169'=> '©', '170'=> 'ª', '171'=> '«', '172'=> '¬',
            '173'=> '­', '174'=> '®', '175'=> '¯', '176'=> '°', '177'=> '±', '178'=> '²', '179'=> '³', '180'=> '´', '181'=> 'µ', '182'=> '¶',
            '183'=> '·', '184'=> '¸', '185'=> '¹', '186'=> 'º', '187'=> '»', '188'=> '¼', '189'=> '½', '190'=> '¾', '191'=> '¿', '192'=> 'À',
            '193'=> 'Á', '194'=> 'Â', '195'=> 'Ã', '196'=> 'Ä', '197'=> 'Å', '198'=> 'Æ', '199'=> 'Ç', '200'=> 'È', '201'=> 'É', '202'=> 'Ê',
            '203'=> 'Ë', '204'=> 'Ì', '205'=> 'Í', '206'=> 'Î', '207'=> 'Ï', '208'=> 'Ð', '209'=> 'Ñ', '210'=> 'Ò', '211'=> 'Ó', '212'=> 'Ô',
            '213'=> 'Õ', '214'=> 'Ö', '215'=> '×', '216'=> 'Ø', '217'=> 'Ù', '218'=> 'Ú', '219'=> 'Û', '220'=> 'Ü', '221'=> 'Ý', '222'=> 'Þ',
            '223'=> 'ß', '224'=> 'à', '225'=> 'á', '226'=> 'â', '227'=> 'ã', '228'=> 'ä', '229'=> 'å', '230'=> 'æ', '231'=> 'ç', '232'=> 'è',
            '233'=> 'é', '234'=> 'ê', '235'=> 'ë', '236'=> 'ì', '237'=> 'í', '238'=> 'î', '239'=> 'ï', '240'=> 'ð', '241'=> 'ñ', '242'=> 'ò',
            '243'=> 'ó', '244'=> 'ô', '245'=> 'õ', '246'=> 'ö', '247'=> '÷', '248'=> 'ø', '249'=> 'ù', '250'=> 'ú', '251'=> 'û', '252'=> 'ü',
            '253'=> 'ý', '254'=> 'þ', '255'=> 'ÿ'
        );

        $search = Array();
        $replace = Array();

        foreach ($map as $s => $r) {
            $search[] = chr((int)$s);
            $replace[] = $r;
        }

        return str_replace($search, $replace, $string);
    }
thelastshadow
  • 3,406
  • 3
  • 33
  • 36
  • Can i use this for project because this is in MIT licenses – PHP Connect Apr 11 '13 at 12:24
  • In general the MIT licence lets you use it in whatever way you like, so long as you don't remove the licence :) – thelastshadow Apr 11 '13 at 12:47
  • 4
    You decided to put a license on what essentially amounts to... an array? – JMTyler Feb 06 '14 at 23:42
  • I just copied the code out of the file it was in and pasted it here. I try to put open licences on as much of the code I write as possible, even when all it amounts to is a useful array. – thelastshadow Feb 07 '14 at 09:35
  • 3
    It doesn't matter what license you place inside an answer, all user content is licensed under **cc by-sa 3.0 with attribution required**. You can see this in the footer. This code is no longer under the MIT license. – NobleUplift Apr 23 '14 at 15:16
  • Nah, it's dual licenced. Use whichever you feel more comfortable with. – thelastshadow Apr 23 '14 at 16:54
  • Also, I should point out that this function is not "fixing ASCII". There are no ASCII characters above 127. The only thing I can see this function doing is mangling Unicode strings. – NobleUplift Apr 23 '14 at 18:31
  • 1
    @NobleUplift I have renamed it to fixMSWord. I'd agree that it does mangle, but if you have the problem this function fixes it does the job, and I've yet to find another solution. – thelastshadow Apr 24 '14 at 07:46
6

We used the following. It deals with a few more special characters.

$text = str_replace(chr(130), ',', $text);    // Baseline single quote
$text = str_replace(chr(132), '"', $text);    // Baseline double quote
$text = str_replace(chr(133), '...', $text);  // Ellipsis
$text = str_replace(chr(145), "'", $text);    // Left single quote
$text = str_replace(chr(146), "'", $text);    // Right single quote
$text = str_replace(chr(147), '"', $text);    // Left double quote
$text = str_replace(chr(148), '"', $text);    // Right double quote

$text = mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8');
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ceejayoz
  • 176,543
  • 40
  • 303
  • 368
  • You should check the encoding of the string `$text` before you run replaces in it. It could _already_ be a Unicode string and you are mangling it. – NobleUplift Apr 23 '14 at 18:53
6

Every single one of the previous answers except for Gumbo's will mangle Unicode strings:

echo convert_smart_quotes("This is Yi: ꑑ. Point ⒒ this breaks Yi. Yi broke–why? I need a longer––point. This makes Han 嗗 mad.");

Results in:

This is Yi: ?''. Point ?'' this breaks Yi. Yi broke?"why? I need a longer?"?"point. This makes Han ?-- mad.

The iconv:

$output = iconv('UTF-8', 'ASCII//TRANSLIT', $input);

Results in:

PHP Notice: iconv(): Detected an illegal character in input string in php shell code on line 1

You can change it to //IGNORE, which will remove the characters, but not translate them.

This is the best way to replace Microsoft quotes encoded in CP1252. If they are in Unicode and you need to replace them, use Gumbo's answer:

function convert_cp1252_to_ascii($input, $default = '') {
    if ($input === null || $input == '') {
        return $default;
    }

    // https://en.wikipedia.org/wiki/UTF-8
    // https://en.wikipedia.org/wiki/ISO/IEC_8859-1
    // https://en.wikipedia.org/wiki/Windows-1252
    // http://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT
    $encoding = mb_detect_encoding($input, array('Windows-1252', 'ISO-8859-1'), true);
    if ($encoding == 'ISO-8859-1' || $encoding == 'Windows-1252') {
        /*
         * Use the search/replace arrays if a character needs to be replaced with
         * something other than its Unicode equivalent.
         */

        $replace = array(
            128 => "E",    // http://www.fileformat.info/info/unicode/char/20AC/index.htm EURO SIGN
            129 => "",     // UNDEFINED
            130 => ",",    // http://www.fileformat.info/info/unicode/char/201A/index.htm SINGLE LOW-9 QUOTATION MARK
            131 => "f",    // http://www.fileformat.info/info/unicode/char/0192/index.htm LATIN SMALL LETTER F WITH HOOK
            132 => ",,",   // http://www.fileformat.info/info/unicode/char/201e/index.htm DOUBLE LOW-9 QUOTATION MARK
            133 => "...",  // http://www.fileformat.info/info/unicode/char/2026/index.htm HORIZONTAL ELLIPSIS
            134 => "t",    // http://www.fileformat.info/info/unicode/char/2020/index.htm DAGGER
            135 => "T",    // http://www.fileformat.info/info/unicode/char/2021/index.htm DOUBLE DAGGER
            136 => "^",    // http://www.fileformat.info/info/unicode/char/02c6/index.htm MODIFIER LETTER CIRCUMFLEX ACCENT
            137 => "%",    // http://www.fileformat.info/info/unicode/char/2030/index.htm PER MILLE SIGN
            138 => "S",    // http://www.fileformat.info/info/unicode/char/0160/index.htm LATIN CAPITAL LETTER S WITH CARON
            139 => "<",    // http://www.fileformat.info/info/unicode/char/2039/index.htm SINGLE LEFT-POINTING ANGLE QUOTATION MARK
            140 => "OE",   // http://www.fileformat.info/info/unicode/char/0152/index.htm LATIN CAPITAL LIGATURE OE
            141 => "",     // UNDEFINED
            142 => "Z",    // http://www.fileformat.info/info/unicode/char/017d/index.htm LATIN CAPITAL LETTER Z WITH CARON
            143 => "",     // UNDEFINED
            144 => "",     // UNDEFINED
            145 => "'",    // http://www.fileformat.info/info/unicode/char/2018/index.htm LEFT SINGLE QUOTATION MARK
            146 => "'",    // http://www.fileformat.info/info/unicode/char/2019/index.htm RIGHT SINGLE QUOTATION MARK
            147 => "\"",   // http://www.fileformat.info/info/unicode/char/201c/index.htm LEFT DOUBLE QUOTATION MARK
            148 => "\"",   // http://www.fileformat.info/info/unicode/char/201d/index.htm RIGHT DOUBLE QUOTATION MARK
            149 => "*",    // http://www.fileformat.info/info/unicode/char/2022/index.htm BULLET
            150 => "-",    // http://www.fileformat.info/info/unicode/char/2013/index.htm EN DASH
            151 => "--",   // http://www.fileformat.info/info/unicode/char/2014/index.htm EM DASH
            152 => "~",    // http://www.fileformat.info/info/unicode/char/02DC/index.htm SMALL TILDE
            153 => "TM",   // http://www.fileformat.info/info/unicode/char/2122/index.htm TRADE MARK SIGN
            154 => "s",    // http://www.fileformat.info/info/unicode/char/0161/index.htm LATIN SMALL LETTER S WITH CARON
            155 => ">",    // http://www.fileformat.info/info/unicode/char/203A/index.htm SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
            156 => "oe",   // http://www.fileformat.info/info/unicode/char/0153/index.htm LATIN SMALL LIGATURE OE
            157 => "",     // UNDEFINED
            158 => "z",    // http://www.fileformat.info/info/unicode/char/017E/index.htm LATIN SMALL LETTER Z WITH CARON
            159 => "Y",    // http://www.fileformat.info/info/unicode/char/0178/index.htm LATIN CAPITAL LETTER Y WITH DIAERESIS
        );

        $find = array();
        foreach (array_keys($replace) as $key) {
            $find[] = chr($key);
        }

        $input = str_replace($find, array_values($replace), $input);
        /*
         * Because ISO-8859-1 and CP1252 are identical except for 0x80 through 0x9F
         * and control characters, always convert from Windows-1252 to UTF-8.
         */
        $input = iconv('Windows-1252', 'UTF-8//IGNORE', $input);
    }
    return $input;
}

Taken from this answer, with some modifications. If you want to control over what you find/replace, use that function.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NobleUplift
  • 5,631
  • 8
  • 45
  • 87