all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* elisp exercise: toggle-letter-case
@ 2008-10-17 21:02 Xah
  2008-10-17 23:29 ` Andreas Politz
  0 siblings, 1 reply; 8+ messages in thread
From: Xah @ 2008-10-17 21:02 UTC (permalink / raw)
  To: help-gnu-emacs

here's a little interesting exercise.

I'm writing a toggle-letter-case function below. However, it has some
problems. (see the doc string). After thinking on this for a while,
the problem seems a bit complex. It'll take perhaps few hours to fix
these, in particular, if it is to cover chars like éÉ èÈ üÜ. Also, it
seems a good solution will require this function to have a state, but
i'm reluctant to introduce a global variable for it.

I'm wondering, if anyone have a better solution?

(defun toggle-letter-case ()
  "Toggle the letter case of current word or text selection.
Toggles from 3 cases: upper case, lower case, title case,
in that order.
Title case means upcase first letter of each word.

Todo:
• this command only consider English alphabets. For example, it may
not work properly if you have éÉ èÈ üÜ.
• It may not work when the first or second letter is a number, e.g.
“1time”.
• It may not work when you only have a single letter. e.g. “A
teapot”."
(interactive)

(save-excursion
(let (pt pos1 pos2 cap1p cap2p (deactivate-mark nil) (case-fold-search
nil)
         )
  (setq pt (point))
  (if (and transient-mark-mode mark-active)
      (setq pos1 (region-beginning)
            pos2 (region-end))
    (setq pos1 (car (bounds-of-thing-at-point 'word))
          pos2 (cdr (bounds-of-thing-at-point 'word))))

;; check 1th and 2th letters cases
  (goto-char pos1)
  (setq cap1p (looking-at "[A-Z]"))
  (goto-char (1+ pos1))
  (setq cap2p (looking-at "[A-Z]"))

  (cond
   ((and (not cap1p) (not cap2p)) (upcase-initials-region pos1 pos2))
   ((and cap1p (not cap2p)) (upcase-region pos1 pos2) )
   ((and cap1p cap2p) (downcase-region pos1 pos2) )
   (t (downcase-region pos1 pos2) )
   )
  )
)
)

PS the above assumes you have transient-mode on.

  Xah
∑ http://xahlee.org/^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
  2008-10-17 21:02 elisp exercise: toggle-letter-case Xah
@ 2008-10-17 23:29 ` Andreas Politz
  2008-10-18  1:06   ` Nikolaj Schumacher
       [not found]   ` <mailman.1411.1224291972.25473.help-gnu-emacs@gnu.org>
  0 siblings, 2 replies; 8+ messages in thread
From: Andreas Politz @ 2008-10-17 23:29 UTC (permalink / raw)
  To: help-gnu-emacs

Xah wrote:
> here's a little interesting exercise.
> 
> I'm writing a toggle-letter-case function below. However, it has some
> problems. (see the doc string). After thinking on this for a while,
> the problem seems a bit complex. It'll take perhaps few hours to fix
> these, in particular, if it is to cover chars like éÉ èÈ üÜ. Also, it
> seems a good solution will require this function to have a state, but
> i'm reluctant to introduce a global variable for it.
> 
> I'm wondering, if anyone have a better solution?
> 
> (defun toggle-letter-case ()
>   "Toggle the letter case of current word or text selection.
> Toggles from 3 cases: upper case, lower case, title case,
> in that order.
> Title case means upcase first letter of each word.
> 
> Todo:
> • this command only consider English alphabets. For example, it may
> not work properly if you have éÉ èÈ üÜ.
> • It may not work when the first or second letter is a number, e.g.
> “1time”.
> • It may not work when you only have a single letter. e.g. “A
> teapot”."
> (interactive)
> 
> (save-excursion
> (let (pt pos1 pos2 cap1p cap2p (deactivate-mark nil) (case-fold-search
> nil)
>          )
>   (setq pt (point))
>   (if (and transient-mark-mode mark-active)
>       (setq pos1 (region-beginning)
>             pos2 (region-end))
>     (setq pos1 (car (bounds-of-thing-at-point 'word))
>           pos2 (cdr (bounds-of-thing-at-point 'word))))
> 
> ;; check 1th and 2th letters cases
>   (goto-char pos1)
>   (setq cap1p (looking-at "[A-Z]"))
>   (goto-char (1+ pos1))
>   (setq cap2p (looking-at "[A-Z]"))
> 
>   (cond
>    ((and (not cap1p) (not cap2p)) (upcase-initials-region pos1 pos2))
>    ((and cap1p (not cap2p)) (upcase-region pos1 pos2) )
>    ((and cap1p cap2p) (downcase-region pos1 pos2) )
>    (t (downcase-region pos1 pos2) )
>    )
>   )
> )
> )
> 
> PS the above assumes you have transient-mode on.
> 
>   Xah
> ∑ http://xahlee.org/
> 
> ☄


Giving the command a state makes it so much easier, because you
don't have to look at the text at all.  Unless you want it to
consider the current textstate.
Anyway, here is how I would do it:
					
(defun up/down/camel-case (&optional start end)
   (interactive (if (and transient-mark-mode mark-active)
		   (list (region-beginning)
			 (region-end))))

   (if (not (eq last-command this-command))
       (put this-command 'call-count 0))
   (let* ((mark (and transient-mark-mode mark-active))
	(call-count (get this-command 'call-count))
	deactivate-mark
	(action
	 (nth call-count
	      (if mark
		  '(downcase-region upcase-region capitalize-region)
		'(downcase-word upcase-word capitalize-word)))))
     (if mark
	(apply action (list start end))
       (funcall action 1)
       (backward-word))
     (put this-command 'call-count (% (1+ call-count) 3))))

-ap



^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
  2008-10-17 23:29 ` Andreas Politz
@ 2008-10-18  1:06   ` Nikolaj Schumacher
       [not found]   ` <mailman.1411.1224291972.25473.help-gnu-emacs@gnu.org>
  1 sibling, 0 replies; 8+ messages in thread
From: Nikolaj Schumacher @ 2008-10-18  1:06 UTC (permalink / raw)
  To: Andreas Politz; +Cc: help-gnu-emacs

Andreas Politz <politza@fh-trier.de> wrote:

> Giving the command a state makes it so much easier, because you
> don't have to look at the text at all.

I think looking at the text is actually less work.  It's only three
lines of code.


(defun toggle-region-case (&optional beg end)
  (interactive (and transient-mark-mode mark-active
                    (list (region-beginning)
                          (region-end))))
  (let ((pt (point))
        case-fold-search
        deactivate-mark)
    (if beg
        (goto-char beg)
      (forward-word)
      (setq end (point))
      (backward-word))
    (cond
     ((looking-at "[[:upper:]][[:upper:]]") (downcase-region (point) end))
     ((looking-at "[[:upper:]]") (upcase-region (point) end))
     (t (upcase-initials-region (point) end)))
    (goto-char pt)))



regards,
Nikolaj Schumacher




^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
       [not found]   ` <mailman.1411.1224291972.25473.help-gnu-emacs@gnu.org>
@ 2008-10-18 18:53     ` Xah
  2008-10-18 20:47       ` Nikolaj Schumacher
  2008-10-18 20:50       ` Xah
  0 siblings, 2 replies; 8+ messages in thread
From: Xah @ 2008-10-18 18:53 UTC (permalink / raw)
  To: help-gnu-emacs

On Oct 17, 6:06 pm, Nikolaj Schumacher <m...@nschum.de> wrote:
> Andreas Politz <poli...@fh-trier.de> wrote:
> > Giving the command a state makes it so much easier, because you
> > don't have to look at the text at all.
>
> I think looking at the text is actually less work.  It's only three
> lines of code.
>
> (defun toggle-region-case (&optional beg end)
>   (interactive (and transient-mark-mode mark-active
>                     (list (region-beginning)
>                           (region-end))))
>   (let ((pt (point))
>         case-fold-search
>         deactivate-mark)
>     (if beg
>         (goto-char beg)
>       (forward-word)
>       (setq end (point))
>       (backward-word))
>     (cond
>      ((looking-at "[[:upper:]][[:upper:]]") (downcase-region (point) end))
>      ((looking-at "[[:upper:]]") (upcase-region (point) end))
>      (t (upcase-initials-region (point) end)))
>     (goto-char pt)))
>
> regards,
> Nikolaj Schumacher

Nik, your solution failed the spec! lol.
It didn't work on single letter case, or words starting with number.

Andreas's solution works. But he did use a state. Minor fixable
problem is that it didn't consider the word/region's current state, so
that sometimes pressing the key doesn't do anything until one invokes
it again.

The use of “[[:upper]]” is great, and the use of properties to keep
state (very nice touch), and the use of “interactive” with optional
args.

Thanks guys.

  Xah
∑ http://xahlee.org/^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
  2008-10-18 18:53     ` Xah
@ 2008-10-18 20:47       ` Nikolaj Schumacher
  2008-10-18 20:50       ` Xah
  1 sibling, 0 replies; 8+ messages in thread
From: Nikolaj Schumacher @ 2008-10-18 20:47 UTC (permalink / raw)
  To: Xah; +Cc: help-gnu-emacs

Xah <xahlee@gmail.com> wrote:

> Nik, your solution failed the spec! lol.
> It didn't work on single letter case, or words starting with number.

So what?  It just does the same as yours in a more concise manner while
fixing the umlaut problem.  I didn't know the challenge for a "better
solution" would include doing all the TODOs for you. :)

Those two special cases should be quickly handled, though.

    (cond
     ((looking-at "\\([^[:alpha:]]*\\)[[:upper:]][[:upper:]]")
      (capitalize-region (match-end 1) end))
     ((looking-at "[^[:alpha:]]*[[:upper:]]") (downcase-region (point) end))
     (t (upcase-region (point) end)))


Although that makes it less readable...
I think I'd prefer this version:


(defun toggle-region-case (&optional beg end)
  "Toggle the case of the word around or after point.
This toggles between downcased, upcased and capitalized.  Optional
argument BEG and END (or an active mark) determine the region to work on
instead of the current word."
  (interactive (and transient-mark-mode mark-active
                    (list (region-beginning)
                          (region-end))))
  (let ((pt (point))
        case-fold-search
        deactivate-mark)
    (if beg
        (goto-char beg)
      ;; The following works in between words. Don't use thingatpt.
      (goto-char (1- (point)))
      (forward-word)
      (setq end (point))
      (backward-word))
    (if (looking-at "\\([^[:alpha:]]*\\)[[:upper:]]\\([[:upper:]]\\)?")
        (if (match-end 2)
            (capitalize-region (match-end 1) end)
          (downcase-region (point) end))
      (upcase-region (point) end))
    (goto-char pt)))

The semantics on regions remain a little vague, too, since only the
first word is looked at.  That doesn't allow for a distinction between:

"A WORD" and "A Word"

Solving that problem properly would make the code a lot more complex.




regards,
Nikolaj Schumacher




^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
  2008-10-18 18:53     ` Xah
  2008-10-18 20:47       ` Nikolaj Schumacher
@ 2008-10-18 20:50       ` Xah
  2008-10-19 15:26         ` Nikolaj Schumacher
       [not found]         ` <mailman.1506.1224429976.25473.help-gnu-emacs@gnu.org>
  1 sibling, 2 replies; 8+ messages in thread
From: Xah @ 2008-10-18 20:50 UTC (permalink / raw)
  To: help-gnu-emacs

Here's the improved version borrowing Andreas and Nikolaj's ideas.

(defun toggle-letter-case ()
  "Toggle the letter case of current word or text selection.
Toggles from 3 cases: UPPER CASE, lower case, Title Case,
in that cyclic order."
(interactive)

(let (pos1 pos2 (deactivate-mark nil) (case-fold-search nil))
  (if (and transient-mark-mode mark-active)
      (setq pos1 (region-beginning)
            pos2 (region-end))
    (setq pos1 (car (bounds-of-thing-at-point 'word))
          pos2 (cdr (bounds-of-thing-at-point 'word))))

  (when (not (eq last-command this-command))
    (save-excursion
      (goto-char pos1)
      (cond
       ((looking-at "[[:lower:]][[:lower:]]") (put this-command 'state
"all lower"))
       ((looking-at "[[:upper:]][[:upper:]]") (put this-command 'state
"all caps") )
       ((looking-at "[[:upper:]][[:lower:]]") (put this-command 'state
"init caps") )
       (t (put this-command 'state "all lower") )
       )
      )
    )

  (cond
   ((string= "all lower" (get this-command 'state)) (upcase-initials-
region pos1 pos2) (put this-command 'state "init caps"))
   ((string= "init caps" (get this-command 'state)) (upcase-region
pos1 pos2) (put this-command 'state "all caps"))
   ((string= "all caps" (get this-command 'state)) (downcase-region
pos1 pos2) (put this-command 'state "all lower"))
   )
)
)

some notes:

it doesn't use (&optional beg end) with the associated
“(interactive ...)” code because i think when a command is designed
only for interactive use, then it makes sense to not support calling
it in elisp as much as possible.

As far as i know, elisp does not have a conventional mechanism to
indicate that a function is ONLY for interactive use. More
specifically, those with “(interactive ...)” clause are properly
called “commands”, meaning that it can be used BOTH by interactive
call as well in elisp code.

Going philosophical on this, i wonder if the system would better if
the presence of “(interactive ...)” is to mean that the command cannot
be allowed in elisp code. This would mean, that all elisp functions
are separated into 2 groups: those commands proper (presence of
“interactive”) and functions proper (no presence of “interactive”).
This also means that some class of functions that are good for both
interactive and elisp use will now have to be separated into 2
versions (i.e. means more coding). But i'm guessing that over all this
is a good thing and does not introduce much more labor.

I think this separation is a good because with that there's a clear
indication which function is for interactive use and which is for
elisp only, and this indication is mechanical, i.e. build into the
system so that calling a command in elisp program won't work. Right
now, when a function is meant for interactive use only, emacs does its
best to warn it in the inline doc or elisp manual.

the above is just musings on the design, of course, in case someone
takes me this to be some modernization of elisp. LOL.

  Xah
∑ http://xahlee.org/^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
  2008-10-18 20:50       ` Xah
@ 2008-10-19 15:26         ` Nikolaj Schumacher
       [not found]         ` <mailman.1506.1224429976.25473.help-gnu-emacs@gnu.org>
  1 sibling, 0 replies; 8+ messages in thread
From: Nikolaj Schumacher @ 2008-10-19 15:26 UTC (permalink / raw)
  To: Xah; +Cc: help-gnu-emacs

Xah <xahlee@gmail.com> wrote:

> it doesn't use (&optional beg end) with the associated
> “(interactive ...)” code because i think when a command is designed
> only for interactive use, then it makes sense to not support calling
> it in elisp as much as possible.

But it limits the code's re-usability.  Imagine someone wants a function
that toggles the case for the current symbol or sentence (instead of
word).

regards,
Nikolaj Schumacher




^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: elisp exercise: toggle-letter-case
       [not found]         ` <mailman.1506.1224429976.25473.help-gnu-emacs@gnu.org>
@ 2008-10-19 17:46           ` Xah
  0 siblings, 0 replies; 8+ messages in thread
From: Xah @ 2008-10-19 17:46 UTC (permalink / raw)
  To: help-gnu-emacs

On Oct 19, 8:26 am, Nikolaj Schumacher <m...@nschum.de> wrote:
> Xah<xah...@gmail.com> wrote:
> > it doesn't use (&optional beg end) with the associated
> > “(interactive ...)” code because i think when a command is designed
> > only for interactive use, then it makes sense to not support calling
> > it in elisp as much as possible.
>
> But it limits the code's re-usability.  Imagine someone wants a function
> that toggles the case for the current symbol or sentence (instead of
> word).

i think this function is not suitable for calling in elisp program
because its behavior is somewhat unpredictable.

When called interactively, user has visual feedback on what's the
current state, so she can decide whether to call again.

When used programmatically, it's rather unpredictable what's would be
the letter case after calling this function unless the programer put
more code to check what are the letter cases to begin with.

If the programer wants to, for example, downcase/upcase/initCap region/
word, it's much simpler by calling elisp's default functions on these,
at most 2 calls.

  Xah
∑ http://xahlee.org/^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2008-10-19 17:46 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2008-10-17 21:02 elisp exercise: toggle-letter-case Xah
2008-10-17 23:29 ` Andreas Politz
2008-10-18  1:06   ` Nikolaj Schumacher
     [not found]   ` <mailman.1411.1224291972.25473.help-gnu-emacs@gnu.org>
2008-10-18 18:53     ` Xah
2008-10-18 20:47       ` Nikolaj Schumacher
2008-10-18 20:50       ` Xah
2008-10-19 15:26         ` Nikolaj Schumacher
     [not found]         ` <mailman.1506.1224429976.25473.help-gnu-emacs@gnu.org>
2008-10-19 17:46           ` Xah

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.