Wordpress - Ignoring initial articles (like 'a', 'an' or 'the') when sorting queries?
An easier way may be to go through and change the permalink slug on those posts that need it (under the title on the post writing screen) and then just use that for ordering instead of the title.
ie. use post_name
not post_title
for sorting...
This would also mean that your permalink may be different if you use %postname% in your permalink structure, which could be an added bonus.
eg. gives http://example.com/rolling-stones/
not http://example.com/the-rolling-stones/
EDIT: code to update the existing slugs, removing the unwanted prefixes from post_name
column...
global $wpdb;
$posttype = 'release';
$stripprefixes = array('a-','an-','the-');
$results = $wpdb->get_results("SELECT ID, post_name FROM ".$wpdb->prefix."posts" WHERE post_type = '".$posttype."' AND post_status = 'publish');
if (count($results) > 0) {
foreach ($results as $result) {
$postid = $result->ID;
$postslug = $result->post_name;
foreach ($stripprefixes as $stripprefix) {
$checkprefix = strtolower(substr($postslug,0,strlen($stripprefix));
if ($checkprefix == $stripprefix) {
$newslug = substr($postslug,strlen($stripprefix),strlen($postslug));
// echo $newslug; // debug point
$query = $wpdb->prepare("UPDATE ".$wpdb->prefix."posts SET post_name = '%s' WHERE ID = '%d'", $newslug, $postid);
$wpdb->query($query);
}
}
}
}
The Problem
I think there's a typo in there:
The name of the filter is posts_fields
not post_fields
.
That could explain why the title2
field is unknown, because it's definition isn't added to the generated SQL string.
Alternative - Single filter
We can rewrite it to use only a single filter:
add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
// Do nothing
if( '_custom' !== $q->get( 'orderby' ) )
return $orderby;
global $wpdb;
$matches = 'The'; // REGEXP is not case sensitive here
// Custom ordering (SQL)
return sprintf(
"
CASE
WHEN {$wpdb->posts}.post_title REGEXP( '^($matches)[[:space:]]+' )
THEN TRIM( SUBSTR( {$wpdb->posts}.post_title FROM %d ))
ELSE {$wpdb->posts}.post_title
END %s
",
strlen( $matches ) + 1,
'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'
);
}, 10, 2 );
where you can now activate the custom ordering with the _custom
orderby parameter:
$args_post = array
'post_type' => 'release',
'orderby' => '_custom', // Activate the custom ordering
'order' => 'ASC',
'posts_per_page' => -1,
);
$loop = new WP_Query($args_post);
while ($loop->have_posts() ) : $loop->the_post();
Alternative - Recursive TRIM()
Let's implement the recursive idea by Pascal Birchler, commented here:
add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
if( '_custom' !== $q->get( 'orderby' ) )
return $orderby;
global $wpdb;
// Adjust this to your needs:
$matches = [ 'the ', 'an ', 'a ' ];
return sprintf(
" %s %s ",
wpse_sql( $matches, " LOWER( {$wpdb->posts}.post_title) " ),
'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'
);
}, 10, 2 );
where we can for example construct the recursive function as:
function wpse_sql( &$matches, $sql )
{
if( empty( $matches ) || ! is_array( $matches ) )
return $sql;
$sql = sprintf( " TRIM( LEADING '%s' FROM ( %s ) ) ", $matches[0], $sql );
array_shift( $matches );
return wpse_sql( $matches, $sql );
}
This means that
$matches = [ 'the ', 'an ', 'a ' ];
echo wpse_sql( $matches, " LOWER( {$wpdb->posts}.post_title) " );
will generate:
TRIM( LEADING 'a ' FROM (
TRIM( LEADING 'an ' FROM (
TRIM( LEADING 'the ' FROM (
LOWER( wp_posts.post_title)
) )
) )
) )
Alternative - MariaDB
In general I like to use MariaDB instead of MySQL. Then it's much easier because MariaDB 10.0.5 supports REGEXP_REPLACE
:
/**
* Ignore (the,an,a) in post title ordering
*
* @uses MariaDB 10.0.5+
*/
add_filter( 'posts_orderby', function( $orderby, \WP_Query $q )
{
if( '_custom' !== $q->get( 'orderby' ) )
return $orderby;
global $wpdb;
return sprintf(
" REGEXP_REPLACE( {$wpdb->posts}.post_title, '^(the|a|an)[[:space:]]+', '' ) %s",
'ASC' === strtoupper( $q->get( 'order' ) ) ? 'ASC' : 'DESC'
);
}, 10, 2 );
EDIT
I have improved the code a bit. All code block are updated accordingly. Just a note though before jumping into the updates in the ORIGINAL ANSWER, I have set up the code to work with the following
Custom post type ->
release
Custom taxonomy ->
game
Make sure to set this according to your needs
ORIGINAL ANSWER
In addition to the other answers and the typo pointed out by @birgire, here is another approach.
First, we will set the title as a hidden custom field, but we will first remove the words like the
that we would want to exclude. Before we do that, we need to first create a helper function in order to remove the banned words from the term names and post titles
/**
* Function get_name_banned_removed()
*
* A helper function to handle removing banned words
*
* @param string $tring String to remove banned words from
* @param array $banned Array of banned words to remove
* @return string $string
*/
function get_name_banned_removed( $string = '', $banned = [] )
{
// Make sure we have a $string to handle
if ( !$string )
return $string;
// Sanitize the string
$string = filter_var( $string, FILTER_SANITIZE_STRING );
// Make sure we have an array of banned words
if ( !$banned
|| !is_array( $banned )
)
return $string;
// Make sure that all banned words is lowercase
$banned = array_map( 'strtolower', $banned );
// Trim the string and explode into an array, remove banned words and implode
$text = trim( $string );
$text = strtolower( $text );
$text_exploded = explode( ' ', $text );
if ( in_array( $text_exploded[0], $banned ) )
unset( $text_exploded[0] );
$text_as_string = implode( ' ', $text_exploded );
return $string = $text_as_string;
}
Now that we have that covered, lets look at the piece of code to set our custom field. You must remove this code completely as soon as you have loaded any page once. If you have a huge site with a ton of posts, you can set posts_per_page
to something to 100
and run the scripts a couple of times until all posts have the custom field have been set to all posts
add_action( 'wp', function ()
{
add_filter( 'posts_fields', function ( $fields, \WP_Query $q )
{
global $wpdb;
remove_filter( current_filter(), __FUNCTION__ );
// Only target a query where the new custom_query parameter is set with a value of custom_meta_1
if ( 'custom_meta_1' === $q->get( 'custom_query' ) ) {
// Only get the ID and post title fields to reduce server load
$fields = "$wpdb->posts.ID, $wpdb->posts.post_title";
}
return $fields;
}, 10, 2);
$args = [
'post_type' => 'release', // Set according to needs
'posts_per_page' => -1, // Set to execute smaller chucks per page load if necessary
'suppress_filters' => false, // Allow the posts_fields filter
'custom_query' => 'custom_meta_1', // New parameter to allow that our filter only target this query
'meta_query' => [
[
'key' => '_custom_sort_post_title', // Make it a hidden custom field
'compare' => 'NOT EXISTS'
]
]
];
$q = get_posts( $args );
// Make sure we have posts before we continue, if not, bail
if ( !$q )
return;
foreach ( $q as $p ) {
$new_post_title = strtolower( $p->post_title );
if ( function_exists( 'get_name_banned_removed' ) )
$new_post_title = get_name_banned_removed( $new_post_title, ['the'] );
// Set our custom field value
add_post_meta(
$p->ID, // Post ID
'_custom_sort_post_title', // Custom field name
$new_post_title // Custom field value
);
} //endforeach $q
});
Now that the custom fields are set to all posts and the code above is removed, we need to make sure that we set the this custom field to all new posts or whenever we update the post title. For this we will use the transition_post_status
hook. The following code can go into a plugin (which I recommend) or in your functions.php
add_action( 'transition_post_status', function ( $new_status, $old_status, $post )
{
// Make sure we only run this for the release post type
if ( 'release' !== $post->post_type )
return;
$text = strtolower( $post->post_title );
if ( function_exists( 'get_name_banned_removed' ) )
$text = get_name_banned_removed( $text, ['the'] );
// Set our custom field value
update_post_meta(
$post->ID, // Post ID
'_custom_sort_post_title', // Custom field name
$text // Custom field value
);
}, 10, 3 );
QUERYING YOUR POSTS
You can run your queries as normal without any custom filters. You can query and sort your posts as follow
$args_post = [
'post_type' => 'release',
'orderby' => 'meta_value',
'meta_key' => '_custom_sort_post_title',
'order' => 'ASC',
'posts_per_page' => -1,
];
$loop = new WP_Query( $args );